Просмотр исходного кода

feat(material): validate flow value (#620)

* feat(material): validate flow value

* fix: required field missing

* feat: validate when variable sync

* feat: license
Yiwei Mao 5 месяцев назад
Родитель
Сommit
bf1bd2e32b

+ 1 - 0
apps/demo-free-layout/src/nodes/code/form-meta.tsx

@@ -27,5 +27,6 @@ export const FormRender = ({ form }: FormRenderProps<CodeNodeJSON>) => (
 export const formMeta: FormMeta = {
   render: (props) => <FormRender {...props} />,
   effect: defaultFormMeta.effect,
+  validate: defaultFormMeta.validate,
   plugins: [createInferInputsPlugin({ sourceKey: 'inputsValues', targetKey: 'inputs' })],
 };

+ 13 - 18
apps/demo-free-layout/src/nodes/default-form-meta.tsx

@@ -3,17 +3,14 @@
  * SPDX-License-Identifier: MIT
  */
 
-import {
-  FormRenderProps,
-  FormMeta,
-  ValidateTrigger,
-  FeedbackLevel,
-} from '@flowgram.ai/free-layout-editor';
+import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';
 import {
   autoRenameRefEffect,
   provideJsonSchemaOutputs,
   syncVariableTitle,
   DisplayOutputs,
+  validateFlowValue,
+  validateWhenVariableSync,
 } from '@flowgram.ai/form-materials';
 import { Divider } from '@douyinfe/semi-ui';
 
@@ -42,18 +39,16 @@ export const defaultFormMeta: FormMeta<FlowNodeJSON> = {
   validate: {
     title: ({ value }) => (value ? undefined : 'Title is required'),
     'inputsValues.*': ({ value, context, formValues, name }) => {
-      const valuePropetyKey = name.replace(/^inputsValues\./, '');
+      const valuePropertyKey = name.replace(/^inputsValues\./, '');
       const required = formValues.inputs?.required || [];
-      if (
-        required.includes(valuePropetyKey) &&
-        (value === '' || value === undefined || value?.content === '')
-      ) {
-        return {
-          message: `${valuePropetyKey} is required`,
-          level: FeedbackLevel.Error, // Error || Warning
-        };
-      }
-      return undefined;
+
+      return validateFlowValue(value, {
+        node: context.node,
+        required: required.includes(valuePropertyKey),
+        errorMessages: {
+          required: `${valuePropertyKey} is required`,
+        },
+      });
     },
   },
   /**
@@ -73,6 +68,6 @@ export const defaultFormMeta: FormMeta<FlowNodeJSON> = {
   effect: {
     title: syncVariableTitle,
     outputs: provideJsonSchemaOutputs,
-    inputsValues: autoRenameRefEffect,
+    inputsValues: [...autoRenameRefEffect, ...validateWhenVariableSync({ scope: 'public' })],
   },
 };

+ 1 - 0
packages/materials/form-materials/bin/materials.ts

@@ -28,6 +28,7 @@ const _types: string[] = [
   'plugins',
   'shared',
   'typings',
+  'validate',
   'form-plugins',
   'hooks',
 ];

+ 1 - 0
packages/materials/form-materials/src/effects/index.ts

@@ -7,3 +7,4 @@ export * from './provide-batch-input';
 export * from './auto-rename-ref';
 export * from './provide-json-schema-outputs';
 export * from './sync-variable-title';
+export * from './validate-when-variable-sync';

+ 5 - 0
packages/materials/form-materials/src/effects/validate-when-variable-sync/config.json

@@ -0,0 +1,5 @@
+{
+  "name": "validate-when-variable-sync",
+  "depMaterials": [],
+  "depPackages": []
+}

+ 35 - 0
packages/materials/form-materials/src/effects/validate-when-variable-sync/index.ts

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { isEmpty } from 'lodash';
+import {
+  DataEvent,
+  Effect,
+  EffectOptions,
+  getNodeScope,
+  getNodePrivateScope,
+} from '@flowgram.ai/editor';
+
+export const validateWhenVariableSync = ({
+  scope,
+}: {
+  scope?: 'private' | 'public';
+} = {}): EffectOptions[] => [
+  {
+    event: DataEvent.onValueInit,
+    effect: (({ context, form }) => {
+      const nodeScope =
+        scope === 'private' ? getNodePrivateScope(context.node) : getNodeScope(context.node);
+
+      const disposable = nodeScope.available.onListOrAnyVarChange(() => {
+        if (!isEmpty(form.state.errors)) {
+          form.validate();
+        }
+      });
+
+      return () => disposable.dispose();
+    }) as Effect,
+  },
+];

+ 1 - 0
packages/materials/form-materials/src/index.ts

@@ -9,3 +9,4 @@ export * from './shared';
 export * from './typings';
 export * from './form-plugins';
 export * from './plugins';
+export * from './validate';

+ 6 - 0
packages/materials/form-materials/src/validate/index.tsx

@@ -0,0 +1,6 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export * from './validate-flow-value';

+ 7 - 0
packages/materials/form-materials/src/validate/validate-flow-value/config.json

@@ -0,0 +1,7 @@
+{
+  "name": "validate-flow-value",
+  "depMaterials": [
+    "flow-value"
+  ],
+  "depPackages": []
+}

+ 73 - 0
packages/materials/form-materials/src/validate/validate-flow-value/index.tsx

@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { isNil, uniq } from 'lodash';
+import { FeedbackLevel, FlowNodeEntity, getNodeScope } from '@flowgram.ai/editor';
+
+import { IFlowTemplateValue, IFlowValue } from '@/typings';
+
+interface Context {
+  node: FlowNodeEntity;
+  required?: boolean;
+  errorMessages?: {
+    required?: string;
+    unknownVariable?: string;
+  };
+}
+
+export function validateFlowValue(value: IFlowValue | undefined, ctx: Context) {
+  const { node, required, errorMessages } = ctx;
+
+  const {
+    required: requiredMessage = 'Field is required',
+    unknownVariable: unknownVariableMessage = 'Unknown Variable',
+  } = errorMessages || {};
+
+  if (required && (isNil(value) || isNil(value?.content) || value?.content === '')) {
+    return {
+      level: FeedbackLevel.Error,
+      message: requiredMessage,
+    };
+  }
+
+  if (value?.type === 'ref') {
+    const variable = getNodeScope(node).available.getByKeyPath(value?.content || []);
+    if (!variable) {
+      return {
+        level: FeedbackLevel.Error,
+        message: unknownVariableMessage,
+      };
+    }
+  }
+
+  if (value?.type === 'template') {
+    const allRefs = getTemplateKeyPaths(value);
+
+    for (const ref of allRefs) {
+      const variable = getNodeScope(node).available.getByKeyPath(ref);
+      if (!variable) {
+        return {
+          level: FeedbackLevel.Error,
+          message: unknownVariableMessage,
+        };
+      }
+    }
+  }
+
+  return undefined;
+}
+
+/**
+ * get template key paths
+ * @param value
+ * @returns
+ */
+function getTemplateKeyPaths(value: IFlowTemplateValue) {
+  // find all keyPath wrapped in {{}}
+  const keyPathReg = /{{(.*?)}}/g;
+  return uniq(value.content?.match(keyPathReg) || []).map((_keyPath) =>
+    _keyPath.slice(2, -2).split('.')
+  );
+}

+ 8 - 0
packages/variable-engine/json-schema/src/json-schema/utils.ts

@@ -52,6 +52,8 @@ export namespace JsonSchemaUtils {
               meta: {
                 title: _property.title,
                 description: _property.description,
+                required: _property.required,
+                default: _property.default,
               },
             })),
         });
@@ -147,6 +149,12 @@ export namespace JsonSchemaUtils {
                 if (property.meta?.description && schema) {
                   schema.description = property.meta.description;
                 }
+                if (property.meta?.default && schema) {
+                  schema.default = property.meta.default;
+                }
+                if (property.meta?.required && schema) {
+                  schema.required = property.meta.required;
+                }
 
                 return [property.key, schema!];
               })