This commit is contained in:
dengqichen 2025-10-21 17:27:10 +08:00
parent 697f0a69bf
commit 63fc41dede
5 changed files with 563 additions and 238 deletions

View File

@ -1,28 +1,20 @@
import React, { useState, useEffect, useMemo } from 'react';
import { FormItem, Input, NumberPicker, Select, FormLayout, Switch } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { createSchemaField, FormProvider, ISchema } from '@formily/react';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Save, RotateCcw } from 'lucide-react';
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { useToast } from '@/components/ui/use-toast';
import type { FlowNode, FlowNodeData } from '../types';
import type { WorkflowNodeDefinition } from '../nodes/types';
import type { WorkflowNodeDefinition, JSONSchema } from '../nodes/types';
import { isConfigurableNode } from '../nodes/types';
// 创建Schema组件
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
NumberPicker,
Select,
FormLayout,
Switch,
'Input.TextArea': Input.TextArea,
},
});
import { convertJsonSchemaToZod, extractDataSourceTypes } from '../utils/schemaConverter';
import { loadDataSource, DataSourceType, type DataSourceOption } from '../utils/dataSourceLoader';
interface NodeConfigModalProps {
visible: boolean;
@ -40,214 +32,313 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
const [loading, setLoading] = useState(false);
const { toast } = useToast();
// 动态数据源缓存
const [dataSourceCache, setDataSourceCache] = useState<Record<string, DataSourceOption[]>>({});
const [loadingDataSources, setLoadingDataSources] = useState(false);
// 获取节点定义
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
// ✅ 根据节点 ID 重新创建表单实例(修复切换节点时数据不更新的问题)
const configForm = useMemo(() => createForm(), [node?.id]);
const inputForm = useMemo(() => createForm(), [node?.id]);
// ✅ 生成 Zod Schema
const configSchema = useMemo(() => {
if (!nodeDefinition?.configSchema) return null;
return convertJsonSchemaToZod(nodeDefinition.configSchema);
}, [nodeDefinition?.configSchema]);
// 初始化表单数据
const inputMappingSchema = useMemo(() => {
if (!nodeDefinition || !isConfigurableNode(nodeDefinition) || !nodeDefinition.inputMappingSchema) {
return null;
}
return convertJsonSchemaToZod(nodeDefinition.inputMappingSchema);
}, [nodeDefinition]);
// ✅ 创建表单实例(基本配置)
const configForm = useForm({
resolver: configSchema ? zodResolver(configSchema) : undefined,
defaultValues: {}
});
// ✅ 创建表单实例(输入映射)
const inputForm = useForm({
resolver: inputMappingSchema ? zodResolver(inputMappingSchema) : undefined,
defaultValues: {}
});
// ✅ 预加载动态数据源
useEffect(() => {
if (!visible || !nodeDefinition) return;
const loadDynamicData = async () => {
const configTypes = extractDataSourceTypes(nodeDefinition.configSchema);
const inputTypes = isConfigurableNode(nodeDefinition) && nodeDefinition.inputMappingSchema
? extractDataSourceTypes(nodeDefinition.inputMappingSchema)
: [];
const allTypes = [...new Set([...configTypes, ...inputTypes])];
if (allTypes.length === 0) {
return;
}
setLoadingDataSources(true);
try {
const cache: Record<string, DataSourceOption[]> = {};
await Promise.all(
allTypes.map(async (type) => {
const data = await loadDataSource(type as DataSourceType);
cache[type] = data;
})
);
setDataSourceCache(cache);
} catch (error) {
console.error('加载动态数据源失败:', error);
toast({
title: '数据加载失败',
description: '无法加载部分选项数据,请刷新重试',
variant: 'destructive'
});
} finally {
setLoadingDataSources(false);
}
};
loadDynamicData();
}, [visible, nodeDefinition, toast]);
// ✅ 初始化表单数据
useEffect(() => {
if (visible && node && nodeDefinition) {
const nodeData = node.data || {};
// 准备默认配置
// 设置基本配置默认值
const defaultConfig = {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description
description: nodeDefinition.description || ''
};
configForm.reset({ ...defaultConfig, ...(nodeData.configs || {}) });
// 设置表单初始值
configForm.setInitialValues({ ...defaultConfig, ...(nodeData.configs || {}) });
configForm.reset();
// 设置输入映射默认值
if (isConfigurableNode(nodeDefinition)) {
inputForm.setInitialValues(nodeData.inputMapping || {});
inputForm.reset();
inputForm.reset(nodeData.inputMapping || {});
}
}
}, [visible, node, nodeDefinition, configForm, inputForm]);
// 递归处理表单值将JSON字符串转换为对象
const processFormValues = (values: Record<string, any>, schema: ISchema | undefined): Record<string, any> => {
const result: Record<string, any> = {};
if (!schema?.properties || typeof schema.properties !== 'object') return values;
Object.entries(values).forEach(([key, value]) => {
const propSchema = (schema.properties as Record<string, any>)?.[key];
// 如果是object类型且值是字符串尝试解析
if (propSchema?.type === 'object' && typeof value === 'string') {
try {
result[key] = JSON.parse(value);
} catch {
result[key] = value; // 解析失败保持原值
}
} else {
result[key] = value;
}
});
return result;
};
const handleSubmit = async () => {
// ✅ 保存配置
const handleSave = () => {
if (!node || !nodeDefinition) return;
try {
// 使用 handleSubmit 验证并获取数据
configForm.handleSubmit(async (configData) => {
setLoading(true);
try {
// 获取输入映射数据
let inputData = {};
if (isConfigurableNode(nodeDefinition) && nodeDefinition.inputMappingSchema) {
inputData = inputForm.getValues();
}
// 获取表单值并转换
const configs = processFormValues(configForm.values, nodeDefinition.configSchema);
const inputMapping = isConfigurableNode(nodeDefinition)
? processFormValues(inputForm.values, nodeDefinition.inputMappingSchema)
: {};
// 构建更新数据
const updatedData: Partial<FlowNodeData> = {
label: configs.nodeName || node.data.label,
configs,
inputMapping,
// outputs 保留原值(不修改,因为它是只读的输出能力定义)
outputs: node.data.outputs || [],
label: (configData as any).nodeName || nodeDefinition.nodeName,
configs: configData as Record<string, any>,
inputMapping: inputData,
outputs: isConfigurableNode(nodeDefinition) ? nodeDefinition.outputs || [] : []
};
onOk(node.id, updatedData);
toast({
title: "保存成功",
description: "节点配置已成功保存",
title: '保存成功',
description: '节点配置已更新'
});
// 关闭抽屉
onCancel();
} catch (error) {
if (error instanceof Error) {
console.error('保存节点配置失败:', error);
toast({
variant: "destructive",
title: "保存失败",
description: error.message,
title: '保存失败',
description: '请检查必填字段是否填写完整',
variant: 'destructive'
});
}
} finally {
setLoading(false);
}
})();
};
// ✅ 重置表单
const handleReset = () => {
configForm.reset();
inputForm.reset();
if (!node || !nodeDefinition) return;
const defaultConfig = {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description || ''
};
configForm.reset(defaultConfig);
inputForm.reset({});
toast({
title: "已重置",
description: "表单已重置为初始值",
title: '已重置',
description: '表单已恢复为默认值'
});
};
// 将JSON Schema转换为Formily Schema扩展配置
const convertToFormilySchema = (jsonSchema: ISchema): ISchema => {
const schema: ISchema = {
type: 'object',
properties: {}
};
// ✅ 渲染字段控件
const renderFieldControl = useCallback((prop: any, field: any, key: string) => {
// 动态数据源
if (prop['x-dataSource']) {
const options = dataSourceCache[prop['x-dataSource']] || [];
return (
<Select
disabled={loadingDataSources || loading}
value={field.value?.toString()}
onValueChange={(value) => {
// 如果是数字类型,转换为数字
const numValue = Number(value);
field.onChange(isNaN(numValue) ? value : numValue);
}}
>
<SelectTrigger>
<SelectValue placeholder={`请选择${prop.title || ''}`} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
if (!jsonSchema.properties || typeof jsonSchema.properties !== 'object') return schema;
// 静态枚举值
if (prop.enum && Array.isArray(prop.enum)) {
return (
<Select
disabled={loading}
value={field.value?.toString()}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder={`请选择${prop.title || ''}`} />
</SelectTrigger>
<SelectContent>
{prop.enum.map((value: any, index: number) => {
const enumValue = typeof value === 'object' ? value.value : value;
const enumLabel = typeof value === 'object' ? value.label : (prop.enumNames?.[index] || value);
return (
<SelectItem key={enumValue} value={enumValue.toString()}>
{enumLabel}
</SelectItem>
);
})}
</SelectContent>
</Select>
);
}
Object.entries(jsonSchema.properties as Record<string, any>).forEach(([key, prop]: [string, any]) => {
const field: any = {
title: prop.title || key,
description: prop.description,
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: prop.description,
// 垂直布局不需要 labelCol 和 wrapperCol
},
};
// 根据类型设置组件
// 根据类型渲染
switch (prop.type) {
case 'string':
if (prop.enum) {
field['x-component'] = 'Select';
field['x-component-props'] = {
style: { width: '100%' }, // 统一宽度
options: prop.enum.map((v: any, i: number) => ({
label: prop.enumNames?.[i] || v,
value: v
}))
};
} else if (prop.format === 'password') {
field['x-component'] = 'Input';
field['x-component-props'] = {
type: 'password',
style: { width: '100%' } // 统一宽度
};
} else {
field['x-component'] = 'Input';
field['x-component-props'] = {
style: { width: '100%' } // 统一宽度
};
if (prop.format === 'textarea' || key.includes('description')) {
return (
<Textarea
{...field}
disabled={loading}
placeholder={prop.description || `请输入${prop.title || ''}`}
rows={3}
/>
);
}
break;
return (
<Input
{...field}
type={prop.format === 'password' ? 'password' : 'text'}
disabled={loading}
placeholder={prop.description || `请输入${prop.title || ''}`}
/>
);
case 'number':
case 'integer':
field['x-component'] = 'NumberPicker';
field['x-component-props'] = {
min: prop.minimum,
max: prop.maximum,
style: { width: '100%' } // 统一宽度
};
break;
return (
<Input
{...field}
type="number"
disabled={loading}
placeholder={prop.description || `请输入${prop.title || ''}`}
onChange={(e) => {
const value = e.target.value;
field.onChange(value === '' ? undefined : Number(value));
}}
/>
);
case 'boolean':
field['x-component'] = 'Switch';
break;
case 'object':
field['x-component'] = 'Input.TextArea';
field['x-component-props'] = {
rows: 4,
style: { width: '100%' }, // 统一宽度
placeholder: '请输入JSON格式例如{"key": "value"}'
};
// ✅ 关键修复将object类型的default值转换为JSON字符串
if (prop.default !== undefined && typeof prop.default === 'object') {
field.default = JSON.stringify(prop.default, null, 2);
} else {
field.default = prop.default;
}
// Formily会自动处理object的序列化
field['x-validator'] = (value: any) => {
if (!value) return true;
if (typeof value === 'string') {
try {
JSON.parse(value);
return true;
} catch {
return '请输入有效的JSON格式';
}
}
return true;
};
break;
return (
<input
type="checkbox"
checked={field.value || false}
disabled={loading}
onChange={(e) => field.onChange(e.target.checked)}
className="h-4 w-4"
/>
);
default:
field['x-component'] = 'Input';
field['x-component-props'] = {
style: { width: '100%' } // 统一宽度
};
return (
<Input
{...field}
disabled={loading}
placeholder={prop.description || `请输入${prop.title || ''}`}
/>
);
}
}, [dataSourceCache, loadingDataSources, loading]);
// ✅ 渲染表单字段
const renderFormFields = useCallback((
schema: JSONSchema,
form: ReturnType<typeof useForm>,
prefix: string = ''
) => {
if (!schema.properties || typeof schema.properties !== 'object') {
return null;
}
// 设置默认值非object类型
if (prop.type !== 'object' && prop.default !== undefined) {
field.default = prop.default;
}
return Object.entries(schema.properties).map(([key, prop]: [string, any]) => {
const fieldName = prefix ? `${prefix}.${key}` : key;
const isRequired = Array.isArray(schema.required) && schema.required.includes(key);
// 设置必填
if (Array.isArray(jsonSchema.required) && jsonSchema.required.includes(key)) {
field.required = true;
}
(schema.properties as Record<string, any>)[key] = field;
return (
<FormField
key={fieldName}
control={form.control}
name={fieldName}
render={({ field }) => (
<FormItem>
<FormLabel>
{prop.title || key}
{isRequired && <span className="text-red-500 ml-1">*</span>}
</FormLabel>
<FormControl>
{renderFieldControl(prop, field, key)}
</FormControl>
{prop.description && (
<FormDescription>{prop.description}</FormDescription>
)}
<FormMessage />
</FormItem>
)}
/>
);
});
return schema;
};
}, [renderFieldControl]);
if (!nodeDefinition) {
return null;
@ -262,7 +353,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
<SheetTitle className="text-xl font-semibold">
- {nodeDefinition.nodeName}
</SheetTitle>
<SheetDescription className="text-sm text-muted-foreground">
<SheetDescription>
</SheetDescription>
</SheetHeader>
@ -270,38 +361,33 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
{/* Content - 可滚动区域 */}
<div className="flex-1 overflow-y-auto px-6 py-4">
{/* 使用 Accordion 支持多个面板同时展开,默认展开基本配置 */}
<Accordion
type="multiple"
defaultValue={["config"]}
className="w-full"
>
{/* 基本配置 - 始终显示 */}
{/* 基本配置 */}
<AccordionItem value="config" className="border-b">
<AccordionTrigger className="text-base font-semibold flex-row-reverse justify-end gap-2 hover:no-underline">
</AccordionTrigger>
<AccordionContent className="px-1">
<FormProvider form={configForm}>
<FormLayout layout="vertical" colon={false}>
<SchemaField schema={convertToFormilySchema(nodeDefinition.configSchema)} />
</FormLayout>
</FormProvider>
<AccordionContent className="px-1 space-y-4">
<Form {...configForm}>
{renderFormFields(nodeDefinition.configSchema, configForm)}
</Form>
</AccordionContent>
</AccordionItem>
{/* 输入映射 - 条件显示 */}
{/* 输入映射 */}
{isConfigurableNode(nodeDefinition) && nodeDefinition.inputMappingSchema && (
<AccordionItem value="input" className="border-b">
<AccordionTrigger className="text-base font-semibold flex-row-reverse justify-end gap-2 hover:no-underline">
</AccordionTrigger>
<AccordionContent className="px-1">
<FormProvider form={inputForm}>
<FormLayout layout="vertical" colon={false}>
<SchemaField schema={convertToFormilySchema(nodeDefinition.inputMappingSchema)} />
</FormLayout>
</FormProvider>
<AccordionContent className="px-1 space-y-4">
<Form {...inputForm}>
{renderFormFields(nodeDefinition.inputMappingSchema, inputForm)}
</Form>
</AccordionContent>
</AccordionItem>
)}
@ -362,15 +448,15 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
</div>
{/* Footer - 固定在底部 */}
<div className="px-6 py-4 border-t bg-background">
<SheetFooter className="flex-row justify-between gap-2">
<SheetFooter className="px-6 py-4 border-t bg-background">
<div className="flex justify-between w-full">
<Button
type="button"
variant="outline"
onClick={handleReset}
className="gap-2"
disabled={loading}
>
<RotateCcw className="h-4 w-4" />
<RotateCcw className="mr-2 h-4 w-4" />
</Button>
<div className="flex gap-2">
@ -378,21 +464,21 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
type="button"
variant="outline"
onClick={onCancel}
disabled={loading}
>
</Button>
<Button
type="button"
onClick={handleSubmit}
disabled={loading}
className="gap-2"
onClick={handleSave}
disabled={loading || loadingDataSources}
>
<Save className="h-4 w-4" />
<Save className="mr-2 h-4 w-4" />
{loading ? '保存中...' : '保存配置'}
</Button>
</div>
</SheetFooter>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
);

View File

@ -1,4 +1,5 @@
import {ConfigurableNodeDefinition, NodeType, NodeCategory} from './types';
import {DataSourceType} from '../utils/dataSourceLoader';
/**
* Jenkins构建节点定义
@ -61,15 +62,14 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
description: "节点的详细说明",
default: "通过Jenkins执行构建任务"
},
jenkinsUrl: {
type: "string",
title: "Jenkins服务器地址",
description: "Jenkins服务器的完整URL地址",
default: "http://jenkins.example.com:8080",
format: "uri"
jenkinsServerId: {
type: "number",
title: "Jenkins服务器",
description: "选择要使用的Jenkins服务器",
'x-dataSource': DataSourceType.JENKINS_SERVERS
}
},
required: ["nodeName", "nodeCode", "jenkinsUrl"]
required: ["nodeName", "nodeCode", "jenkinsServerId"]
},
// ✅ 输出能力定义(只读展示,传递给后端)
outputs: [

View File

@ -0,0 +1,122 @@
import request from '@/utils/request';
/**
*
*/
export enum DataSourceType {
JENKINS_SERVERS = 'JENKINS_SERVERS',
K8S_CLUSTERS = 'K8S_CLUSTERS',
GIT_REPOSITORIES = 'GIT_REPOSITORIES',
DOCKER_REGISTRIES = 'DOCKER_REGISTRIES'
}
/**
*
*/
export interface DataSourceConfig {
url: string;
params?: Record<string, any>;
transform: (data: any) => Array<{ label: string; value: any; [key: string]: any }>;
}
/**
*
*/
export interface DataSourceOption {
label: string;
value: any;
[key: string]: any;
}
/**
*
*/
export const DATA_SOURCE_REGISTRY: Record<DataSourceType, DataSourceConfig> = {
[DataSourceType.JENKINS_SERVERS]: {
url: '/api/v1/external-system/list',
params: { type: 'JENKINS', enabled: true },
transform: (data: any[]) => {
return data.map((item: any) => ({
label: `${item.name} (${item.url})`,
value: item.id,
url: item.url,
name: item.name
}));
}
},
[DataSourceType.K8S_CLUSTERS]: {
url: '/api/v1/k8s-cluster/list',
params: { enabled: true },
transform: (data: any[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.id,
apiServer: item.apiServer
}));
}
},
[DataSourceType.GIT_REPOSITORIES]: {
url: '/api/v1/git-repo/list',
params: { enabled: true },
transform: (data: any[]) => {
return data.map((item: any) => ({
label: `${item.name} (${item.url})`,
value: item.id,
url: item.url
}));
}
},
[DataSourceType.DOCKER_REGISTRIES]: {
url: '/api/v1/docker-registry/list',
params: { enabled: true },
transform: (data: any[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.id,
url: item.url
}));
}
}
};
/**
*
* @param type
* @returns
*/
export const loadDataSource = async (type: DataSourceType): Promise<DataSourceOption[]> => {
const config = DATA_SOURCE_REGISTRY[type];
if (!config) {
console.error(`数据源类型 ${type} 未配置`);
return [];
}
try {
const response = await request.get(config.url, { params: config.params });
// request 拦截器已经提取了 data 字段response 直接是数组
return config.transform(response || []);
} catch (error) {
console.error(`加载数据源 ${type} 失败:`, error);
return [];
}
};
/**
*
* @param types
* @returns
*/
export const loadMultipleDataSources = async (
types: DataSourceType[]
): Promise<Record<DataSourceType, DataSourceOption[]>> => {
const results = await Promise.all(
types.map(type => loadDataSource(type))
);
return types.reduce((acc, type, index) => {
acc[type] = results[index];
return acc;
}, {} as Record<DataSourceType, DataSourceOption[]>);
};

View File

@ -0,0 +1,117 @@
import { z } from 'zod';
import type { JSONSchema } from '../nodes/types';
/**
* JSON Schema Zod Schema
* @param jsonSchema JSON Schema
* @returns Zod Schema
*/
export const convertJsonSchemaToZod = (jsonSchema: JSONSchema): z.ZodObject<any> => {
const shape: Record<string, z.ZodTypeAny> = {};
if (!jsonSchema.properties || typeof jsonSchema.properties !== 'object') {
return z.object({});
}
Object.entries(jsonSchema.properties).forEach(([key, prop]: [string, any]) => {
let fieldSchema: z.ZodTypeAny;
// 根据类型创建 Zod Schema
switch (prop.type) {
case 'string':
fieldSchema = z.string();
if (prop.format === 'email') {
fieldSchema = (fieldSchema as z.ZodString).email(prop.title ? `${prop.title}格式不正确` : '邮箱格式不正确');
} else if (prop.format === 'url') {
fieldSchema = (fieldSchema as z.ZodString).url(prop.title ? `${prop.title}格式不正确` : 'URL格式不正确');
}
if (prop.minLength) {
fieldSchema = (fieldSchema as z.ZodString).min(prop.minLength, `${prop.title || key}至少需要${prop.minLength}个字符`);
}
if (prop.maxLength) {
fieldSchema = (fieldSchema as z.ZodString).max(prop.maxLength, `${prop.title || key}最多${prop.maxLength}个字符`);
}
if (prop.pattern) {
fieldSchema = (fieldSchema as z.ZodString).regex(new RegExp(prop.pattern), `${prop.title || key}格式不正确`);
}
break;
case 'number':
case 'integer':
fieldSchema = prop.type === 'integer' ? z.number().int() : z.number();
if (prop.minimum !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).min(prop.minimum, `${prop.title || key}不能小于${prop.minimum}`);
}
if (prop.maximum !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).max(prop.maximum, `${prop.title || key}不能大于${prop.maximum}`);
}
break;
case 'boolean':
fieldSchema = z.boolean();
break;
case 'array':
fieldSchema = z.array(z.any());
if (prop.minItems !== undefined) {
fieldSchema = (fieldSchema as z.ZodArray<any>).min(prop.minItems, `${prop.title || key}至少需要${prop.minItems}`);
}
if (prop.maxItems !== undefined) {
fieldSchema = (fieldSchema as z.ZodArray<any>).max(prop.maxItems, `${prop.title || key}最多${prop.maxItems}`);
}
break;
case 'object':
fieldSchema = z.record(z.any());
break;
default:
fieldSchema = z.any();
}
// 处理默认值
if (prop.default !== undefined) {
fieldSchema = fieldSchema.default(prop.default);
}
// 处理枚举
if (prop.enum && Array.isArray(prop.enum)) {
const enumValues = prop.enum.map((v: any) => (typeof v === 'object' ? v.value : v));
fieldSchema = z.enum(enumValues as [string, ...string[]]);
}
// 处理必填字段
if (Array.isArray(jsonSchema.required) && jsonSchema.required.includes(key)) {
// 已经是必填的,不需要额外处理
} else {
// 非必填字段设为 optional
fieldSchema = fieldSchema.optional();
}
shape[key] = fieldSchema;
});
return z.object(shape);
};
/**
* JSON Schema
* @param jsonSchema JSON Schema
* @returns
*/
export const extractDataSourceTypes = (jsonSchema: JSONSchema): string[] => {
const types: string[] = [];
if (!jsonSchema.properties || typeof jsonSchema.properties !== 'object') {
return types;
}
Object.values(jsonSchema.properties).forEach((prop: any) => {
if (prop['x-dataSource']) {
types.push(prop['x-dataSource']);
}
});
return types;
};