Przeglądaj źródła

feat(material): json-schema-editor optimize (#662)

* feat(material): json-schema-editor optimize

* feat: remove useless config.json

* feat: print install script
Yiwei Mao 5 miesięcy temu
rodzic
commit
c91f70c1a6

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

@@ -72,7 +72,6 @@ program
     let { packagesToInstall } = copyMaterial(material, projectInfo);
 
     // 4. Install the dependencies
-    packagesToInstall.push(`@flowgram.ai/editor`);
     packagesToInstall = packagesToInstall.map((_pkg) => {
       if (
         _pkg.startsWith(`@flowgram.ai/`) &&

+ 4 - 0
packages/materials/form-materials/bin/project.ts

@@ -70,22 +70,26 @@ export function findRushJson(startPath: string): string | null {
 
 export function installDependencies(packages: string[], projectInfo: ProjectInfo): void {
   if (fs.existsSync(path.join(projectInfo.projectPath, 'yarn.lock'))) {
+    console.log(`yarn add ${packages.join(' ')}`);
     execSync(`yarn add ${packages.join(' ')}`, { stdio: 'inherit' });
     return;
   }
 
   if (fs.existsSync(path.join(projectInfo.projectPath, 'pnpm-lock.yaml'))) {
+    console.log(`pnpm add ${packages.join(' ')}`);
     execSync(`pnpm add ${packages.join(' ')}`, { stdio: 'inherit' });
     return;
   }
 
   //  rush monorepo
   if (findRushJson(projectInfo.projectPath)) {
+    console.log(`rush add ${packages.map((pkg) => `--package ${pkg}`).join(' ')}`);
     execSync(`rush add ${packages.map((pkg) => `--package ${pkg}`).join(' ')}`, {
       stdio: 'inherit',
     });
     return;
   }
 
+  console.log(`npm install ${packages.join(' ')}`);
   execSync(`npm install ${packages.join(' ')}`, { stdio: 'inherit' });
 }

+ 9 - 104
packages/materials/form-materials/src/components/json-schema-editor/default-value.tsx

@@ -3,27 +3,19 @@
  * SPDX-License-Identifier: MIT
  */
 
-import React, { useRef, useState, useCallback } from 'react';
+import React from 'react';
 
 import { IJsonSchema } from '@flowgram.ai/json-schema';
-import { IconButton, JsonViewer, Tooltip } from '@douyinfe/semi-ui';
-import { IconBrackets } from '@douyinfe/semi-icons';
+import { I18n } from '@flowgram.ai/editor';
 
 import { ConstantInput } from '@/components/constant-input';
 
-import { getValueType } from './utils';
-import {
-  ConstantInputWrapper,
-  JSONHeader,
-  JSONHeaderLeft,
-  JSONHeaderRight,
-  JSONViewerWrapper,
-} from './styles';
+import { ConstantInputWrapper } from './styles';
 
 /**
- * 根据不同的数据类型渲染对应的默认值输入组件。
- * @param props - 组件属性,包括 value, type, placeholder, onChange。
- * @returns 返回对应类型的输入组件或 null。
+ * Renders the corresponding default value input component based on different data types.
+ * @param props - Component properties, including value, type, placeholder, onChange.
+ * @returns Returns the input component of the corresponding type or null.
  */
 export function DefaultValue(props: {
   value: any;
@@ -34,102 +26,15 @@ export function DefaultValue(props: {
   jsonFormatText?: string;
   onChange: (value: any) => void;
 }) {
-  const { value, schema, type, onChange, placeholder, jsonFormatText } = props;
+  const { value, schema, onChange, placeholder } = props;
 
-  const wrapperRef = useRef<HTMLDivElement>(null);
-  const JsonViewerRef = useRef<JsonViewer>(null);
-
-  // 为 JsonViewer 添加状态管理
-  const [internalJsonValue, setInternalJsonValue] = useState<string>(
-    getValueType(value) === 'string' ? value : ''
-  );
-
-  // 使用 useCallback 创建稳定的回调函数
-  const handleJsonChange = useCallback((val: string) => {
-    // 只在值真正改变时才更新状态
-    if (val !== internalJsonValue) {
-      setInternalJsonValue(val);
-    }
-  }, []);
-
-  // 处理编辑完成事件
-  const handleEditComplete = useCallback(() => {
-    // 只有当存在key,编辑完成时才触发父组件的 onChange
-    onChange(internalJsonValue);
-    // 确保在更新后移除焦点
-    requestAnimationFrame(() => {
-      // JsonViewerRef.current?.format();
-      wrapperRef.current?.blur();
-    });
-    setJsonReadOnly(true);
-  }, [internalJsonValue, onChange]);
-
-  const [jsonReadOnly, setJsonReadOnly] = useState<boolean>(true);
-
-  const handleFormatJson = useCallback(() => {
-    try {
-      const parsed = JSON.parse(internalJsonValue);
-      const formatted = JSON.stringify(parsed, null, 4);
-      setInternalJsonValue(formatted);
-      onChange(formatted);
-    } catch (error) {
-      console.error('Invalid JSON:', error);
-    }
-  }, [internalJsonValue, onChange]);
-
-  return type === 'object' ? (
-    <>
-      <JSONHeader>
-        <JSONHeaderLeft>json</JSONHeaderLeft>
-        <JSONHeaderRight>
-          <Tooltip content={jsonFormatText ?? 'Format'}>
-            <IconButton
-              icon={<IconBrackets style={{ color: 'var(--semi-color-primary)' }} />}
-              size="small"
-              type="tertiary"
-              theme="borderless"
-              onClick={handleFormatJson}
-            />
-          </Tooltip>
-        </JSONHeaderRight>
-      </JSONHeader>
-
-      <JSONViewerWrapper
-        ref={wrapperRef}
-        tabIndex={-1}
-        onBlur={(e) => {
-          if (wrapperRef.current && !wrapperRef.current?.contains(e.relatedTarget as Node)) {
-            handleEditComplete();
-          }
-        }}
-        onClick={(e: React.MouseEvent) => {
-          setJsonReadOnly(false);
-        }}
-      >
-        <JsonViewer
-          ref={JsonViewerRef}
-          value={getValueType(value) === 'string' ? value : ''}
-          height={120}
-          width="100%"
-          showSearch={false}
-          options={{
-            readOnly: jsonReadOnly,
-            formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' },
-          }}
-          style={{
-            padding: 0,
-          }}
-          onChange={handleJsonChange}
-        />
-      </JSONViewerWrapper>
-    </>
-  ) : (
+  return (
     <ConstantInputWrapper>
       <ConstantInput
         value={value}
         onChange={(_v) => onChange(_v)}
         schema={schema || { type: 'string' }}
-        placeholder={placeholder ?? 'Default value if parameter is not provided'}
+        placeholder={placeholder ?? I18n.t('Default value if parameter is not provided')}
       />
     </ConstantInputWrapper>
   );

+ 53 - 94
packages/materials/form-materials/src/components/json-schema-editor/hooks.tsx

@@ -3,10 +3,11 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useState } from 'react';
 
-import { omit } from 'lodash';
-import { IJsonSchema } from '@flowgram.ai/json-schema';
+import { difference, omit } from 'lodash';
+import { produce } from 'immer';
+import { IJsonSchema, type JsonSchemaTypeManager, useTypeManager } from '@flowgram.ai/json-schema';
 
 import { PropertyValueType } from './types';
 
@@ -15,92 +16,46 @@ function genId() {
   return _id++;
 }
 
-function getDrilldownSchema(
-  value?: PropertyValueType,
-  path?: (keyof PropertyValueType)[]
-): { schema?: PropertyValueType | null; path?: (keyof PropertyValueType)[] } {
-  if (!value) {
-    return {};
-  }
-
-  if (value.type === 'array' && value.items) {
-    return getDrilldownSchema(value.items, [...(path || []), 'items']);
-  }
-
-  return { schema: value, path };
-}
-
 export function usePropertiesEdit(
   value?: PropertyValueType,
   onChange?: (value: PropertyValueType) => void
 ) {
-  // Get drilldown (array.items.items...)
-  const drilldown = useMemo(() => getDrilldownSchema(value), [value, value?.type, value?.items]);
-
-  const isDrilldownObject = drilldown.schema?.type === 'object';
-
-  // Generate Init Property List
-  const initPropertyList = useMemo(
-    () =>
-      isDrilldownObject
-        ? Object.entries(drilldown.schema?.properties || {})
-            .sort(([, a], [, b]) => (a.extra?.index ?? 0) - (b.extra?.index ?? 0))
-            .map(
-              ([name, _value], index) =>
-                ({
-                  key: genId(),
-                  name,
-                  isPropertyRequired: drilldown.schema?.required?.includes(name) || false,
-                  ..._value,
-                  extra: {
-                    ...(_value.extra || {}),
-                    index,
-                  },
-                } as PropertyValueType)
-            )
-        : [],
-    [isDrilldownObject]
-  );
-
-  const [propertyList, setPropertyList] = useState<PropertyValueType[]>(initPropertyList);
-
-  const mountRef = useRef(false);
+  const typeManager = useTypeManager() as JsonSchemaTypeManager;
+
+  // Get drilldown properties (array.items.items.properties...)
+  const drilldownSchema = typeManager.getPropertiesParent(value || {});
+  const canAddField = typeManager.canAddField(value || {});
+
+  const [propertyList, setPropertyList] = useState<PropertyValueType[]>([]);
 
   useEffect(() => {
-    // If initRef is true, it means the component has been mounted
-    if (mountRef.current) {
-      // If the value is changed, update the property list
-      setPropertyList((_list) => {
-        const nameMap = new Map<string, PropertyValueType>();
-
-        for (const _property of _list) {
-          if (_property.name) {
-            nameMap.set(_property.name, _property);
-          }
-        }
-        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);
-            if (_property) {
-              return {
-                key: _property.key,
-                name,
-                isPropertyRequired: drilldown.schema?.required?.includes(name) || false,
-                ..._value,
-              };
-            }
-            return {
-              key: genId(),
-              name,
-              isPropertyRequired: drilldown.schema?.required?.includes(name) || false,
-              ..._value,
-            };
-          });
-      });
-    }
-    mountRef.current = true;
-  }, [drilldown.schema]);
+    // If the value is changed, update the property list
+    setPropertyList((_list) => {
+      const newNames = Object.entries(drilldownSchema?.properties || {})
+        .sort(([, a], [, b]) => (a.extra?.index ?? 0) - (b.extra?.index ?? 0))
+        .map(([key]) => key);
+
+      const oldNames = _list.map((item) => item.name).filter(Boolean) as string[];
+      const addNames = difference(newNames, oldNames);
+
+      return _list
+        .filter((item) => !item.name || newNames.includes(item.name))
+        .map((item) => ({
+          key: item.key,
+          name: item.name,
+          isPropertyRequired: drilldownSchema?.required?.includes(item.name || '') || false,
+          ...item,
+        }))
+        .concat(
+          addNames.map((_name) => ({
+            key: genId(),
+            name: _name,
+            isPropertyRequired: drilldownSchema?.required?.includes(_name) || false,
+            ...(drilldownSchema?.properties?.[_name] || {}),
+          }))
+        );
+    });
+  }, [drilldownSchema]);
 
   const updatePropertyList = (updater: (list: PropertyValueType[]) => PropertyValueType[]) => {
     setPropertyList((_list) => {
@@ -122,21 +77,25 @@ export function usePropertiesEdit(
         }
       }
 
-      let drilldownSchema = value || {};
-      if (drilldown.path) {
-        drilldownSchema = drilldown.path.reduce((acc, key) => acc[key], value || {});
-      }
-      drilldownSchema.properties = nextProperties;
-      drilldownSchema.required = nextRequired;
+      onChange?.(
+        produce(value || {}, (draft) => {
+          const propertiesParent = typeManager.getPropertiesParent(draft);
 
-      onChange?.(value || {});
+          if (propertiesParent) {
+            propertiesParent.properties = nextProperties;
+            propertiesParent.required = nextRequired;
+            return;
+          }
+        })
+      );
 
       return next;
     });
   };
 
   const onAddProperty = () => {
-    updatePropertyList((_list) => [
+    // set property list only, not trigger updatePropertyList
+    setPropertyList((_list) => [
       ..._list,
       { key: genId(), name: '', type: 'string', extra: { index: _list.length + 1 } },
     ]);
@@ -153,14 +112,14 @@ export function usePropertiesEdit(
   };
 
   useEffect(() => {
-    if (!isDrilldownObject) {
+    if (!canAddField) {
       setPropertyList([]);
     }
-  }, [isDrilldownObject]);
+  }, [canAddField]);
 
   return {
     propertyList,
-    isDrilldownObject,
+    canAddField,
     onAddProperty,
     onRemoveProperty,
     onEditProperty,

+ 5 - 5
packages/materials/form-materials/src/components/json-schema-editor/index.tsx

@@ -80,7 +80,7 @@ export function JsonSchemaEditor(props: {
         icon={<IconPlus />}
         onClick={onAddProperty}
       >
-        {config?.addButtonText ?? 'Add'}
+        {config?.addButtonText ?? I18n.t('Add')}
       </Button>
     </UIContainer>
   );
@@ -122,7 +122,7 @@ function PropertyEdit(props: {
 
   const typeSelectorValue = useMemo(() => ({ type, items }), [type, items]);
 
-  const { propertyList, isDrilldownObject, onAddProperty, onRemoveProperty, onEditProperty } =
+  const { propertyList, canAddField, onAddProperty, onRemoveProperty, onEditProperty } =
     usePropertiesEdit(value, onChangeProps);
 
   const onChange = (key: string, _value: any) => {
@@ -132,7 +132,7 @@ function PropertyEdit(props: {
     });
   };
 
-  const showCollapse = isDrilldownObject && propertyList.length > 0;
+  const showCollapse = canAddField && propertyList.length > 0;
 
   return (
     <>
@@ -198,7 +198,7 @@ function PropertyEdit(props: {
                   setExpand((_expand) => !_expand);
                 }}
               />
-              {isDrilldownObject && (
+              {canAddField && (
                 <IconButton
                   disabled={readonly}
                   size="small"
@@ -231,7 +231,7 @@ function PropertyEdit(props: {
                   config?.descPlaceholder ?? I18n.t('Help LLM to understand the property')
                 }
               />
-              {$level === 0 && type && type !== 'array' && (
+              {$level === 0 && (
                 <>
                   <UILabel style={{ marginTop: 10 }}>
                     {config?.defaultValueTitle ?? I18n.t('Default Value')}

+ 0 - 29
packages/materials/form-materials/src/components/json-schema-editor/styles.tsx

@@ -200,35 +200,6 @@ export const DefaultValueWrapper = styled.div`
   margin: 0;
 `;
 
-export const JSONViewerWrapper = styled.div`
-  padding: 0 0 24px;
-  &:first-child {
-    margin-top: 0px;
-  }
-`;
-
-export const JSONHeader = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  background-color: var(--semi-color-fill-0);
-  border-radius: 6px 6px 0 0;
-  height: 36px;
-  padding: 0 8px 0 12px;
-`;
-
-export const JSONHeaderLeft = styled.div`
-  display: flex;
-  align-items: center;
-  gap: 10px;
-`;
-
-export const JSONHeaderRight = styled.div`
-  display: flex;
-  align-items: center;
-  gap: 10px;
-`;
-
 export const ConstantInputWrapper = styled.div`
   flex-grow: 1;
 

+ 0 - 29
packages/materials/form-materials/src/components/json-schema-editor/utils.ts

@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
- * SPDX-License-Identifier: MIT
- */
-
-/**
- * Return the corresponding string description according to the type of the input value.根据输入值的类型返回对应的字符串描述。
- * @param value - 需要判断类型的值。The value whose type needs to be judged.
- * @returns 返回值的类型字符串 The type string of the return value('string', 'integer', 'number', 'boolean', 'object', 'array', 'other')。
- */
-export function getValueType(value: any): string {
-  const type = typeof value;
-
-  if (type === 'string') {
-    return 'string';
-  } else if (type === 'number') {
-    return Number.isInteger(value) ? 'integer' : 'number';
-  } else if (type === 'boolean') {
-    return 'boolean';
-  } else if (type === 'object') {
-    if (value === null) {
-      return 'other';
-    }
-    return Array.isArray(value) ? 'array' : 'object';
-  } else {
-    // undefined, function, symbol, bigint etc.
-    return 'other';
-  }
-}

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

@@ -1,7 +0,0 @@
-{
-  "name": "flow-value",
-  "depMaterials": [],
-  "depPackages": [
-    "@flowgram.ai/json-schema"
-  ]
-}

+ 10 - 0
packages/variable-engine/json-schema/src/json-schema/json-schema-type-manager.tsx

@@ -124,8 +124,18 @@ export class JsonSchemaTypeManager<
     return registry?.getTypeSchemaProperties(type);
   };
 
+  public getPropertiesParent = (type: Schema) => {
+    const registry = this.getTypeBySchema(type);
+    return registry?.getPropertiesParent(type);
+  };
+
   public getJsonPaths = (type: Schema) => {
     const registry = this.getTypeBySchema(type);
     return registry?.getJsonPaths(type);
   };
+
+  public canAddField = (type: Schema) => {
+    const registry = this.getTypeBySchema(type);
+    return registry?.canAddField(type);
+  };
 }