浏览代码

添加wizard表单校验逻辑

herj 2 年之前
父节点
当前提交
53c6826030

+ 1 - 0
packages/jldbq-extenison/package.json

@@ -43,6 +43,7 @@
     "moment": "^2.29.4",
     "react": "^17.0.1",
     "react-beautiful-dnd": "^13.1.0",
+    "react-hook-form": "^7.34.1",
     "react-select": "^5.4.0",
     "react-syntax-highlighter": "^15.5.0",
     "swr": "^1.3.0"

+ 69 - 220
packages/jldbq-extenison/src/wizard/StepFourForm.tsx

@@ -1,54 +1,7 @@
-import React, {
-  forwardRef,
-  useImperativeHandle,
-  useRef,
-  useState
-} from 'react';
+import React, { forwardRef, useState } from 'react';
+import { FormNumberInput, Input, IStepForm, Select, useWizard } from './form';
 import { JsonSchemaDrawer } from './drawer';
 
-const FormNumberInput: React.FunctionComponent<{
-  id?: string;
-  value?: number;
-  onChange: (value: number) => void;
-}> = ({ id, value, onChange }) => {
-  const valStr = value !== undefined ? value.toString() : '';
-  const [draft, setDraft] = useState(valStr);
-  const [isEditing, setIsEditing] = useState(false);
-  const inputRef = useRef<HTMLInputElement>(null);
-
-  // getDerivedProps
-  if (!isEditing && valStr !== draft) {
-    setDraft(valStr);
-  }
-
-  return (
-    <div className="form-input-number">
-      <button type="button" onClick={() => onChange((value || 0) - 1)} />
-      <input
-        className="form-input"
-        type="text"
-        autoComplete="off"
-        ref={inputRef}
-        id={id}
-        value={draft}
-        onFocus={() => {
-          inputRef.current?.select();
-          setIsEditing(true);
-        }}
-        onBlur={() => {
-          const newVal = parseInt(draft, 10);
-          if (!isNaN(newVal)) {
-            onChange(newVal);
-          }
-          setIsEditing(false);
-        }}
-        onChange={evt => setDraft(evt.target.value)}
-      />
-      <button type="button" onClick={() => onChange((value || 0) + 1)} />
-    </div>
-  );
-};
-
 interface IForm {
   route: string;
   block: string;
@@ -61,175 +14,75 @@ interface IForm {
   taskName: string;
 }
 
-// TODO:
-const checkForm = (formData: any): any => {
-  return null;
-};
-
-const StepFourForm: React.ForwardRefRenderFunction<
-  {
-    getData: () => Partial<IForm> | null;
-  },
-  { wizardData: any[] }
-> = ({ wizardData }, ref) => {
+const StepFourForm: IStepForm<IForm> = (props, ref) => {
   const [showSideBar, setShowSideBar] = useState(false);
-  const [formData, setFormData] = useState<Partial<IForm>>({});
-  const [, setFormError] = useState<any>({});
-
-  useImperativeHandle(ref, () => {
-    return {
-      getData: () => {
-        const err = checkForm(formData);
-        if (err) {
-          setFormError(err);
-          return null;
-        }
-        return formData;
-      }
-    };
-  });
+  const { wizardData, formProps } = useWizard(ref);
+  const { watch } = formProps;
+  const formData = watch();
 
   return (
-    <form>
-      <div className="form-group">
-        <label
-          className="form-label field-required"
-          htmlFor="step-2-datasource"
-        >
-          路由策略
-        </label>
-        <select
-          className="form-input"
-          id="step-4-route"
-          value={formData.route ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, route: evt.target.value })
-          }
-        >
-          <option value="" />
-          <option value="first">第一个</option>
-        </select>
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-4-block">
-          阻塞处理方式
-        </label>
-        <select
-          className="form-input"
-          id="step-4-block"
-          value={formData.block ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, block: evt.target.value })
-          }
-        >
-          <option value="" />
-          <option value="single">单机串行</option>
-        </select>
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-4-timeout">
-          超时时间
-        </label>
-        <FormNumberInput
-          id="step-4-timeout"
-          value={formData.timeout}
-          onChange={value =>
-            setFormData({ ...formData, timeout: Math.max(value, 0) })
-          }
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-4-retry">
-          失败重试次数
-        </label>
-        <FormNumberInput
-          id="step-4-retry"
-          value={formData.retry}
-          onChange={value =>
-            setFormData({ ...formData, retry: Math.max(value, 0) })
-          }
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-4-starttime">
-          增量开始时间
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-4-starttime"
-          autoComplete="off"
-          placeholder="2022.8.6 17:22:14"
-          value={formData.startTime ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, startTime: evt.target.value })
-          }
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label" htmlFor="step-4-timefield">
-          增量时间字段
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-4-timefield"
-          autoComplete="off"
-          placeholder="-DlastTime='%s' -DcurrentTime='%s'"
-          value={formData.timeField ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, timeField: evt.target.value })
-          }
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label" htmlFor="step-4-shardfield">
-          分区字段
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-4-shardfield"
-          autoComplete="off"
-          placeholder="手动输入分区字段"
-          value={formData.shardField ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, shardField: evt.target.value })
-          }
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-4-timerule">
-          定时规则
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-4-timerule"
-          autoComplete="off"
-          placeholder="0 0/2***?"
-          value={formData.timeRule ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, timeRule: evt.target.value })
-          }
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-4-taskname">
-          同步任务名称
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-4-taskname"
-          autoComplete="off"
-          placeholder="同步任务名称"
-          value={formData.taskName ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, taskName: evt.target.value })
-          }
-        />
-      </div>
+    <form onSubmit={evt => evt.preventDefault()}>
+      <Select<IForm>
+        label="路由策略"
+        name="route"
+        options={[{ label: '第一个', value: 'first' }]}
+        required
+        formProps={formProps}
+      />
+      <Select<IForm>
+        label="阻塞处理方式"
+        name="block"
+        options={[{ label: '单机串行', value: 'single' }]}
+        required
+        formProps={formProps}
+      />
+      <FormNumberInput<IForm>
+        label="超时时间"
+        name="timeout"
+        positive
+        required
+        formProps={formProps}
+      />
+      <FormNumberInput<IForm>
+        label="失败重试次数"
+        name="retry"
+        positive
+        required
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="增量开始时间"
+        name="startTime"
+        placeholder="2022.8.6 17:22:14"
+        required
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="增量时间字段"
+        name="timeField"
+        placeholder="-DlastTime='%s' -DcurrentTime='%s'"
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="分区字段"
+        name="shardField"
+        placeholder="分区字段"
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="定时规则"
+        name="timeRule"
+        placeholder="0 0/2***?"
+        required
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="同步任务名称"
+        name="taskName"
+        placeholder="同步任务名称"
+        required
+        formProps={formProps}
+      />
       <button
         type="button"
         tabIndex={-1}
@@ -239,7 +92,7 @@ const StepFourForm: React.ForwardRefRenderFunction<
         构建预览
       </button>
       <JsonSchemaDrawer
-        jsonSchema={wizardData.slice(0, 3).concat(formData)}
+        jsonSchema={{ ...wizardData, step4: formData }}
         show={showSideBar}
         onClose={() => setShowSideBar(false)}
       />
@@ -247,8 +100,4 @@ const StepFourForm: React.ForwardRefRenderFunction<
   );
 };
 
-export const formConfig = {
-  desc: '步骤4: 设置同步参数'
-};
-
 export default forwardRef(StepFourForm);

+ 101 - 189
packages/jldbq-extenison/src/wizard/StepOneForm.tsx

@@ -1,225 +1,141 @@
-import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import React, { forwardRef, useState } from 'react';
+import { UseFormReturn } from 'react-hook-form';
+import { Input, IStepForm, Select, Textarea, useWizard } from './form';
 import { useTableNames } from './service';
 import { TableShemaDrawer } from './drawer';
 
 interface IMysqlDetail {
+  tableName: string;
   field: string;
   pkey: string;
   stmt: string;
 }
 
 interface IHiveDetail {
+  tableName: string;
   path: string;
   hdfs: string;
   filetype: string;
   delim: string;
 }
 
-interface IForm extends IMysqlDetail, IHiveDetail {
-  dataSource: string;
-  tableName: string;
+interface IForm {
+  dataSource1: string;
+  detail1: IMysqlDetail | IHiveDetail;
 }
 
 const MysqlDetail: React.FunctionComponent<{
-  value: Partial<IMysqlDetail>;
-  onChange: (value: Partial<IMysqlDetail>) => void;
-}> = ({ value, onChange }) => {
+  formProps: UseFormReturn<IForm>;
+}> = ({ formProps }) => {
+  const { data: tableNames } = useTableNames('mysql');
+
   return (
     <>
-      <div className="form-group">
-        <label className="form-label" htmlFor="step-1-fieldname">
-          查询字段
-        </label>
-        <textarea
-          className="form-input"
-          id="step-1-fieldname"
-          placeholder="SQL查询, 一般用于多表关联查询时才用"
-          value={value.field ?? ''}
-          onChange={evt => onChange({ field: evt.target.value })}
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label" htmlFor="step-1-pkey">
-          切分主键
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-1-pkey"
-          autoComplete="off"
-          value={value.pkey ?? ''}
-          onChange={evt => onChange({ pkey: evt.target.value })}
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label" htmlFor="step-1-stmt">
-          条件语句
-        </label>
-        <textarea
-          className="form-input"
-          id="step-1-stmt"
-          placeholder="where条件"
-          value={value.stmt ?? ''}
-          onChange={evt => onChange({ stmt: evt.target.value })}
-        />
-      </div>
+      <Select<IForm>
+        label="选择表"
+        name="detail1.tableName"
+        options={tableNames ? tableNames.map(value => ({ value })) : []}
+        required
+        formProps={formProps}
+      />
+      <Textarea<IForm>
+        label="查询字段"
+        name="detail1.field"
+        placeholder="SQL查询, 一般用于多表关联查询时才用"
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="切分主键"
+        name="detail1.pkey"
+        formProps={formProps}
+      />
+      <Textarea<IForm>
+        label="条件语句"
+        name="detail1.stmt"
+        placeholder="where条件"
+        formProps={formProps}
+      />
     </>
   );
 };
 
 const HiveDetail: React.FunctionComponent<{
-  value: Partial<IHiveDetail>;
-  onChange: (value: Partial<IHiveDetail>) => void;
-}> = ({ value, onChange }) => {
+  formProps: UseFormReturn<IForm>;
+}> = ({ formProps }) => {
+  const { data: tableNames } = useTableNames('hive');
+
   return (
     <>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-1-path">
-          选择路径
-        </label>
-        <textarea
-          className="form-input"
-          id="step-1-path"
-          placeholder="要读取的文件路径,如果要读取多个⽂件,可使用正确的表达式*"
-          value={value.path ?? ''}
-          onChange={evt => onChange({ path: evt.target.value })}
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-1-hdfs">
-          HDFS
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-1-hdfs"
-          autoComplete="off"
-          value={value.hdfs ?? ''}
-          onChange={evt => onChange({ hdfs: evt.target.value })}
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-1-filetype">
-          文件类型
-        </label>
-        <select
-          className="form-input"
-          id="step-1-filetype"
-          value={value.filetype ?? ''}
-          onChange={evt => onChange({ filetype: evt.target.value })}
-        >
-          <option value="" />
-          <option value="text">text</option>
-          <option value="orc">orc</option>
-        </select>
-      </div>
-      <div className="form-group">
-        <label className="form-label" htmlFor="step-1-delim">
-          分隔符
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-1-delim"
-          autoComplete="off"
-          value={value.delim ?? ''}
-          onChange={evt => onChange({ delim: evt.target.value })}
-        />
-      </div>
+      <Select<IForm>
+        label="选择表"
+        name="detail1.tableName"
+        options={tableNames ? tableNames.map(value => ({ value })) : []}
+        required
+        formProps={formProps}
+      />
+      <Textarea<IForm>
+        label="选择路径"
+        name="detail1.path"
+        placeholder="要读取的文件路径,如果要读取多个⽂件,可使用正确的表达式*"
+        required
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="HDFS"
+        name="detail1.hdfs"
+        placeholder="hdfs namenode节点地址"
+        required
+        formProps={formProps}
+      />
+      <Select<IForm>
+        label="文件类型"
+        name="detail1.filetype"
+        options={[{ value: 'text' }, { value: 'orc' }]}
+        required
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="分隔符"
+        name="detail1.delim"
+        required
+        formProps={formProps}
+      />
     </>
   );
 };
 
-// TODO:
-const checkForm = (formData: any): any => {
-  return null;
-};
-
-const StepOneForm: React.ForwardRefRenderFunction<
-  {
-    getData: () => Partial<IForm> | null;
-  },
-  { wizardData: any[] }
-> = (props, ref) => {
+const StepOneForm: IStepForm<IForm> = (props, ref) => {
   const [showSideBar, setShowSideBar] = useState(false);
-  const [formData, setFormData] = useState<Partial<IForm>>({});
-  const [, setFormError] = useState<any>({});
-  const { data: tableNames } = useTableNames(formData.dataSource);
-  // const loading = !tableNames && !fetchError;
+  const { formProps } = useWizard(ref);
+  const { watch, resetField, setValue } = formProps;
+  const dataSource = watch('dataSource1');
+  const tableName = watch('detail1.tableName');
 
-  useImperativeHandle(ref, () => {
-    return {
-      getData: () => {
-        const err = checkForm(formData);
-        if (err) {
-          setFormError(err);
-          return null;
-        }
-        return formData;
-      }
-    };
-  });
+  const resetDetail = () => {
+    resetField('detail1');
+    setValue('detail1.tableName', '');
+  };
 
   let detail: JSX.Element | null = null;
-  if (formData.dataSource === 'mysql') {
-    detail = (
-      <MysqlDetail
-        value={formData}
-        onChange={value => setFormData({ ...formData, ...value })}
-      />
-    );
-  } else if (formData.dataSource === 'hive') {
-    detail = (
-      <HiveDetail
-        value={formData}
-        onChange={value => setFormData({ ...formData, ...value })}
-      />
-    );
+  if (dataSource === 'mysql') {
+    detail = <MysqlDetail formProps={formProps} />;
+  } else if (dataSource === 'hive') {
+    detail = <HiveDetail formProps={formProps} />;
   }
 
   return (
-    <form>
-      <div className="form-group">
-        <label
-          className="form-label field-required"
-          htmlFor="step-1-datasource"
-        >
-          选择数据源
-        </label>
-        <select
-          className="form-input"
-          id="step-1-datasource"
-          value={formData.dataSource ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, dataSource: evt.target.value })
-          }
-        >
-          <option value="" />
-          <option value="mysql">MySQL</option>
-          <option value="hive">Hive</option>
-        </select>
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-1-datatable">
-          选择表
-        </label>
-        <select
-          className="form-input"
-          id="step-1-datatable"
-          value={formData.tableName ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, tableName: evt.target.value })
-          }
-        >
-          <option value="" />
-          {tableNames &&
-            tableNames.map(tn => (
-              <option key={tn} value={tn}>
-                {tn}
-              </option>
-            ))}
-        </select>
-      </div>
+    <form onSubmit={evt => evt.preventDefault()}>
+      <Select<IForm>
+        label="选择数据源"
+        name="dataSource1"
+        options={[
+          { label: 'MySQL', value: 'mysql' },
+          { label: 'Hive', value: 'hive' }
+        ]}
+        required
+        formProps={formProps}
+        changeCallback={resetDetail}
+      />
       {detail}
       <button
         type="button"
@@ -230,8 +146,8 @@ const StepOneForm: React.ForwardRefRenderFunction<
         表结构预览
       </button>
       <TableShemaDrawer
-        dataSource={formData.dataSource}
-        tableName={formData.tableName}
+        dataSource={dataSource}
+        tableName={tableName}
         show={showSideBar}
         onClose={() => setShowSideBar(false)}
       />
@@ -239,8 +155,4 @@ const StepOneForm: React.ForwardRefRenderFunction<
   );
 };
 
-export const formConfig = {
-  desc: '步骤1: 配置提取源'
-};
-
 export default forwardRef(StepOneForm);

+ 61 - 418
packages/jldbq-extenison/src/wizard/StepThreeForm.tsx

@@ -1,128 +1,43 @@
-import React, {
-  forwardRef,
-  useEffect,
-  useImperativeHandle,
-  useState
-} from 'react';
+import React, { forwardRef } from 'react';
 import {
-  DragDropContext,
-  Draggable,
-  Droppable,
-  DropResult
-} from 'react-beautiful-dnd';
+  FormDndSortList,
+  IMapping,
+  IStepForm,
+  PlaceHolderItem,
+  Select,
+  useWizard
+} from './form';
 import { ITableSchemaResponse, useTableSchema } from './service';
 
-interface IMapping {
-  sourceSchema: ITableSchemaResponse;
-  destSchema: ITableSchemaResponse;
-}
-
 interface IForm {
-  mapping: IMapping[];
+  mapping: IMapping<ITableSchemaResponse>[];
   mode: string;
 }
 
-// TODO:
-const checkForm = (formData: any): any => {
-  return null;
-};
-
-// TODO: 同步表名变化状态
-const useSchemaList = (
-  ds?: string,
-  tn?: string
-): [
-  ITableSchemaResponse[],
-  {
-    reorder: (startIndex: number, endIndex: number) => void;
-    remove: (indices: number[]) => void;
-    prepend: (value: ITableSchemaResponse[]) => void;
-  },
-  number
-] => {
-  const { data: schema } = useTableSchema(ds, tn);
-  const [data, setData] = useState<ITableSchemaResponse[]>();
-  const [size, setSize] = useState(0);
-
-  useEffect(() => {
-    if (data || !schema || schema.length === 0) {
-      return;
-    }
-    setData(schema);
-    setSize(schema.length);
-  }, [data, schema]);
-
-  const reorder = (startIndex: number, endIndex: number) => {
-    const result = Array.from(data || []);
-    const [removed] = result.splice(startIndex, 1);
-    result.splice(endIndex, 0, removed);
-    setData(result);
-  };
-
-  const remove = (indices: number[]) => {
-    const idx = new Set(indices);
-    const result = (data || []).filter((v, i) => !idx.has(i));
-    setData(result);
-  };
-
-  const prepend = (value: ITableSchemaResponse[]) => {
-    const result = value.concat(data || []);
-    setData(result);
-  };
-
-  return [data || [], { reorder, remove, prepend }, size];
-};
-
-interface IPreviousFormData {
-  dataSource?: string;
-  tableName?: string;
-}
+const StepThreeForm: IStepForm<IForm> = (props, ref) => {
+  const { wizardData, formProps } = useWizard(ref, { mapping: [] });
 
-const StepThreeForm: React.ForwardRefRenderFunction<
-  {
-    getData: () => Partial<IForm> | null;
-  },
-  { wizardData: any[] }
-> = ({ wizardData }, ref) => {
-  const {
-    dataSource: ds1,
-    tableName: tn1
-  } = wizardData[0] as IPreviousFormData;
-  const {
-    dataSource: ds2,
-    tableName: tn2
-  } = wizardData[1] as IPreviousFormData;
+  const ds1: string | undefined = wizardData['step1']?.dataSource1;
+  const tn1: string | undefined = wizardData['step1']?.detail1?.tableName;
+  const ds2: string | undefined = wizardData['step2']?.dataSource2;
+  const tn2: string | undefined = wizardData['step2']?.detail2?.tableName;
 
-  const [formData, setFormData] = useState<Partial<IForm>>({});
-  const [selection, setSelection] = useState(new Map<string, IMapping>());
-  const [schema1, updater1, size1] = useSchemaList(ds1, tn1);
-  const [schema2, updater2, size2] = useSchemaList(ds2, tn2);
-  const [, setFormError] = useState<any>({});
-
-  const maxListLength = Math.max(size1, size2) || 5;
-  const listLength = Math.max(schema1.length, schema2.length);
-
-  useImperativeHandle(ref, () => {
-    return {
-      getData: () => {
-        const err = checkForm(formData);
-        if (err) {
-          setFormError(err);
-          return null;
-        }
-        return formData;
-      }
-    };
-  });
+  const { data: sourceSchema } = useTableSchema(ds1, tn1);
+  const { data: destSchema } = useTableSchema(ds2, tn2);
 
   const createRowId = (
-    idx: number,
-    sourceSchema?: ITableSchemaResponse,
-    destSchema?: ITableSchemaResponse
+    sourceSchema?: ITableSchemaResponse | PlaceHolderItem,
+    destSchema?: ITableSchemaResponse,
+    idx?: number
   ): { sourceId: string; destId: string; rowId: string } => {
-    const sourceId = sourceSchema
-      ? `${ds1}-${tn1}-${sourceSchema.field}`
-      : `${ds1}-${tn1}-source${idx}`;
+    let sourceId: string;
+    if (sourceSchema instanceof PlaceHolderItem) {
+      sourceId = `${ds1}-${tn1}-${sourceSchema.id}`;
+    } else if (sourceSchema) {
+      sourceId = `${ds1}-${tn1}-${sourceSchema.field}`;
+    } else {
+      sourceId = `${ds1}-${tn1}-source${idx}`;
+    }
     const destId = destSchema
       ? `${ds2}-${tn2}-${destSchema.field}`
       : `${ds2}-${tn2}-dest${idx}`;
@@ -131,328 +46,56 @@ const StepThreeForm: React.ForwardRefRenderFunction<
   };
 
   const formatItemName = (
-    tableName: string,
-    schema: ITableSchemaResponse
+    schema: ITableSchemaResponse,
+    tableName?: string
   ): string => {
-    return `${tableName}${schema.field}.${schema.type}`;
-  };
-
-  const onDragEnd = (result: DropResult) => {
-    if (!result.destination) {
-      return;
-    }
-    const idx1 = result.source.index;
-    const idx2 = result.destination.index;
-    const sourceId1 = `${ds1}-${tn1}-${schema1[idx1].field}`;
-    const sourceId2 = `${ds1}-${tn1}-${schema1[idx2].field}`;
-    const destId1 = `${ds2}-${tn2}-${schema2[idx1].field}`;
-    const destId2 = `${ds2}-${tn2}-${schema2[idx2].field}`;
-    const rowId1 = `${sourceId1}-${destId1}`;
-    const rowId2 = `${sourceId2}-${destId2}`;
-    const newSelState = new Map(selection);
-    newSelState.delete(rowId1);
-    newSelState.delete(rowId2);
-    setSelection(newSelState);
-    updater1.reorder(idx1, idx2);
-  };
-
-  const handleAddData = () => {
-    const indices: number[] = [];
-    for (let i = 0; i < listLength; i++) {
-      const sourceSchema = schema1[i];
-      const destSchema = schema2[i];
-      const sourceId = `${ds1}-${tn1}-${sourceSchema.field}`;
-      const destId = `${ds2}-${tn2}-${destSchema.field}`;
-      const rowId = `${sourceId}-${destId}`;
-      if (selection.has(rowId)) {
-        indices.push(i);
-      }
-    }
-    updater1.remove(indices);
-    updater2.remove(indices);
-    const oldMapping = formData.mapping || [];
-    const mapping = [...oldMapping, ...Array.from(selection.values())];
-    setFormData({ ...formData, mapping });
-    setSelection(new Map());
-  };
-
-  const handleCheckbox = (
-    checked: boolean,
-    rowId: string,
-    sourceSchema: ITableSchemaResponse,
-    destSchema: ITableSchemaResponse
-  ) => {
-    const newState = new Map(selection);
-    if (checked) {
-      newState.set(rowId, { sourceSchema, destSchema });
-    } else {
-      newState.delete(rowId);
-    }
-    setSelection(newState);
+    return `${tableName || ''}${schema.field}.${schema.type}`;
   };
 
   const matchField = (
-    sourceSchema?: ITableSchemaResponse,
+    sourceSchema?: ITableSchemaResponse | PlaceHolderItem,
     destSchema?: ITableSchemaResponse
-  ): boolean | undefined => {
+  ): boolean => {
     return (
-      sourceSchema &&
-      destSchema &&
+      !!sourceSchema &&
+      !!destSchema &&
+      !(sourceSchema instanceof PlaceHolderItem) &&
       sourceSchema.type === destSchema.type &&
       sourceSchema.length === destSchema.length
     );
   };
 
-  const selectable = new Map<string, IMapping>();
-  const handleSelectAll = () => {
-    if (selectable.size === 0) {
-      setSelection(new Map());
-    } else {
-      const newState = new Map(selection);
-      selectable.forEach((v, k) => newState.set(k, v));
-      setSelection(newState);
-    }
-  };
-
-  const handleRemoveMapping = (row: IMapping) => {
-    const mapping = formData.mapping!.filter(mapping => row !== mapping);
-    setFormData({ ...formData, mapping });
-    updater1.prepend([row.sourceSchema]);
-    updater2.prepend([row.destSchema]);
-  };
-
-  const handleRemoveAllMapping = () => {
-    if (!formData.mapping) {
-      return;
-    }
-    const source = formData.mapping.map(row => row.sourceSchema);
-    const dest = formData.mapping.map(row => row.destSchema);
-    updater1.prepend(source);
-    updater2.prepend(dest);
-    setFormData({ ...formData, mapping: [] });
-  };
-
-  const checkList: JSX.Element[] = [];
-  const hintList: JSX.Element[] = [];
-  const sourceList: JSX.Element[] = [];
-  const destList: JSX.Element[] = [];
-
-  for (let i = 0; i < maxListLength; i++) {
-    const sourceSchema = schema1[i] as ITableSchemaResponse | undefined;
-    const destSchema = schema2[i] as ITableSchemaResponse | undefined;
-    const isSchemaMatch = matchField(sourceSchema, destSchema);
-    const { sourceId, destId, rowId } = createRowId(
-      i,
-      sourceSchema,
-      destSchema
-    );
-
-    if (sourceSchema && destSchema && !selection.has(rowId) && isSchemaMatch) {
-      selectable.set(rowId, {
-        sourceSchema,
-        destSchema
-      });
-    }
-
-    if (sourceSchema && destSchema) {
-      checkList.push(
-        <div key={rowId} className="form-list-row">
-          <label className="form-list-check">
-            <input
-              type="checkbox"
-              disabled={!isSchemaMatch}
-              checked={selection.has(rowId)}
-              onChange={evt =>
-                handleCheckbox(
-                  evt.target.checked,
-                  rowId,
-                  sourceSchema,
-                  destSchema
-                )
-              }
-            />
-          </label>
-        </div>
-      );
-      hintList.push(
-        <div key={rowId} className="form-list-row">
-          <div
-            className={
-              'form-link-separator ' +
-              (isSchemaMatch ? '' : 'form-list-item-diabled')
-            }
-          />
-        </div>
-      );
-    } else {
-      checkList.push(
-        <div key={rowId} className="form-list-row">
-          <label className="form-list-check" />
-        </div>
-      );
-      hintList.push(
-        <div key={rowId} className="form-list-row">
-          <div className="form-link-separator form-link-empty" />
-        </div>
-      );
-    }
-    if (sourceSchema) {
-      sourceList.push(
-        <Draggable key={sourceId} draggableId={sourceId} index={i}>
-          {(provided, snapshot) => (
-            <div
-              className="form-list-row"
-              ref={provided.innerRef}
-              {...provided.draggableProps}
-              {...provided.dragHandleProps}
-              tabIndex={-1}
-            >
-              <div className="form-list-item">
-                <span className="form-list-cell">
-                  {formatItemName(tn1!, sourceSchema)}
-                </span>
-              </div>
-            </div>
-          )}
-        </Draggable>
-      );
-    } else {
-      sourceList.push(
-        <div key={sourceId} className="form-list-row">
-          <div className="form-list-item">
-            <span className="form-list-cell form-list-cell-empty" />
-          </div>
-        </div>
-      );
-    }
-    if (destSchema) {
-      destList.push(
-        <div key={destId} className="form-list-row">
-          <div className="form-list-item">
-            <span className="form-list-cell">
-              {formatItemName(tn2!, destSchema)}
-            </span>
-          </div>
-        </div>
-      );
-    } else {
-      destList.push(
-        <div key={destId} className="form-list-row">
-          <div className="form-list-item">
-            <span className="form-list-cell form-list-cell-empty" />
-          </div>
-        </div>
-      );
-    }
-  }
-
-  const mappingList = formData.mapping
-    ? formData.mapping.map((row, idx) => {
-        const { rowId } = createRowId(idx, row.sourceSchema, row.destSchema);
-        const desc1 = formatItemName(tn1!, row.sourceSchema);
-        const desc2 = formatItemName(tn2!, row.destSchema);
-        return (
-          <div key={rowId} className="form-list-row">
-            <div className="form-list-item">
-              <span className="form-list-cell">{desc1}</span>
-              <span className="form-link-separator" />
-              <span className="form-list-cell">{desc2}</span>
-            </div>
-            <button
-              type="button"
-              className="form-list-clear"
-              onClick={() => handleRemoveMapping(row)}
-            />
-          </div>
-        );
-      })
-    : [];
-
   return (
-    <form>
+    <form onSubmit={evt => evt.preventDefault()}>
       <div className="form-group">
         <p className="form-hint">拖拽提取源数据表中的字段,与加载源数据表</p>
       </div>
-      <div className="form-group">
-        <div className="form-list">
-          <div className="form-list-header">
-            <button
-              type="button"
-              className="form-list-check"
-              onClick={handleSelectAll}
-            >
-              全选
-            </button>
-            <div className="form-list-item">
-              <span className="form-list-cell">提取源:{tn1}</span>
-              <span className="form-link-separator" />
-              <span className="form-list-cell">加载源:{tn2}</span>
-            </div>
-          </div>
-          <div className="form-list-body">
-            <div className="form-list-checklist">{checkList}</div>
-            <DragDropContext onDragEnd={onDragEnd}>
-              <Droppable droppableId="form-list-sourcelist">
-                {(provided, snapshot) => (
-                  <div
-                    className="form-list-sourcelist"
-                    ref={provided.innerRef}
-                    {...provided.droppableProps}
-                  >
-                    {sourceList}
-                    {provided.placeholder}
-                  </div>
-                )}
-              </Droppable>
-            </DragDropContext>
-            <div className="form-list-hintlist">{hintList}</div>
-            <div className="form-list-destlist">{destList}</div>
-          </div>
-        </div>
-        <div className="form-arrow-separator">
-          <button
-            type="button"
-            className="form-list-add-btn"
-            onClick={handleAddData}
-          />
-        </div>
-        <div className="form-list">
-          <div className="form-list-header">
-            <div className="form-list-item">
-              <span className="form-list-cell">映射关系:</span>
-            </div>
-            <button
-              type="button"
-              className="form-list-clear"
-              onClick={handleRemoveAllMapping}
-            >
-              清空
-            </button>
-          </div>
-          <div className="form-list-body">{mappingList}</div>
-        </div>
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-3-mode">
-          写入模式
-        </label>
-        <select
-          className="form-input"
-          id="step-3-mode"
-          value={formData.mode ?? ''}
-          onChange={evt => setFormData({ ...formData, mode: evt.target.value })}
-        >
-          <option value="" />
-          <option value="append">追加</option>
-          <option value="overwrite">覆盖</option>
-        </select>
-      </div>
+      <FormDndSortList<ITableSchemaResponse, IForm>
+        initSourceList={sourceSchema || []}
+        initDestList={destSchema || []}
+        sourceName={tn1 || ''}
+        destName={tn2 || ''}
+        matchFn={matchField}
+        createRowId={createRowId}
+        formatItemName={(value, isSource) =>
+          formatItemName(value, isSource ? tn1 : tn2)
+        }
+        name="mapping"
+        required
+        formProps={formProps}
+      />
+      <Select<IForm>
+        label="写入模式"
+        name="mode"
+        options={[
+          { label: '追加', value: 'append' },
+          { label: '覆盖', value: 'overwrite' }
+        ]}
+        required
+        formProps={formProps}
+      />
     </form>
   );
 };
 
-export const formConfig = {
-  desc: '步骤3: 配置转换规则'
-};
-
 export default forwardRef(StepThreeForm);

+ 96 - 158
packages/jldbq-extenison/src/wizard/StepTwoForm.tsx

@@ -1,186 +1,128 @@
-import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import React, { forwardRef, useState } from 'react';
+import { UseFormReturn } from 'react-hook-form';
+import { Input, IStepForm, Select, Textarea, useWizard } from './form';
 import { TableShemaDrawer } from './drawer';
 import { useTableNames } from './service';
 
 interface IMysqlDetail {
+  tableName: string;
   preSql: string;
 }
 
 interface IHiveDetail {
+  tableName: string;
   path: string;
   hdfs: string;
   filetype: string;
   delim: string;
 }
 
-interface IForm extends IMysqlDetail, IHiveDetail {
-  dataSource: string;
-  tableName: string;
+interface IForm {
+  dataSource2: string;
+  detail2: IMysqlDetail | IHiveDetail;
 }
 
-const HiveDetail: React.FunctionComponent<{
-  value: Partial<IHiveDetail>;
-  onChange: (value: Partial<IHiveDetail>) => void;
-}> = ({ value, onChange }) => {
+const MysqlDetail: React.FunctionComponent<{
+  formProps: UseFormReturn<IForm>;
+}> = ({ formProps }) => {
+  const { data: tableNames } = useTableNames('mysql');
+
   return (
     <>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-1-path">
-          选择路径
-        </label>
-        <textarea
-          className="form-input"
-          id="step-1-path"
-          placeholder="要读取的文件路径,如果要读取多个⽂件,可使用正确的表达式*"
-          value={value.path ?? ''}
-          onChange={evt => onChange({ path: evt.target.value })}
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-1-hdfs">
-          HDFS
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-1-hdfs"
-          autoComplete="off"
-          value={value.hdfs ?? ''}
-          onChange={evt => onChange({ hdfs: evt.target.value })}
-        />
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-1-filetype">
-          文件类型
-        </label>
-        <select
-          className="form-input"
-          id="step-1-filetype"
-          value={value.filetype ?? ''}
-          onChange={evt => onChange({ filetype: evt.target.value })}
-        >
-          <option value="" />
-          <option value="text">text</option>
-          <option value="orc">orc</option>
-        </select>
-      </div>
-      <div className="form-group">
-        <label className="form-label" htmlFor="step-1-delim">
-          分隔符
-        </label>
-        <input
-          className="form-input"
-          type="text"
-          id="step-1-delim"
-          autoComplete="off"
-          value={value.delim ?? ''}
-          onChange={evt => onChange({ delim: evt.target.value })}
-        />
-      </div>
+      <Select<IForm>
+        label="选择表"
+        name="detail2.tableName"
+        options={tableNames ? tableNames.map(value => ({ value })) : []}
+        required
+        formProps={formProps}
+      />
+      <Textarea<IForm>
+        label="preSQL"
+        name="detail2.preSql"
+        placeholder="preSQL"
+        formProps={formProps}
+      />
     </>
   );
 };
 
-// TODO:
-const checkForm = (formData: any): any => {
-  return null;
+const HiveDetail: React.FunctionComponent<{
+  formProps: UseFormReturn<IForm>;
+}> = ({ formProps }) => {
+  const { data: tableNames } = useTableNames('hive');
+
+  return (
+    <>
+      <Select<IForm>
+        label="选择表"
+        name="detail2.tableName"
+        options={tableNames ? tableNames.map(value => ({ value })) : []}
+        required
+        formProps={formProps}
+      />
+      <Textarea<IForm>
+        label="选择路径"
+        name="detail2.path"
+        placeholder="要读取的文件路径,如果要读取多个⽂件,可使用正确的表达式*"
+        required
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="HDFS"
+        name="detail2.hdfs"
+        placeholder="hdfs namenode节点地址"
+        required
+        formProps={formProps}
+      />
+      <Select<IForm>
+        label="文件类型"
+        name="detail2.filetype"
+        options={[{ value: 'text' }, { value: 'orc' }]}
+        required
+        formProps={formProps}
+      />
+      <Input<IForm>
+        label="分隔符"
+        name="detail2.delim"
+        required
+        formProps={formProps}
+      />
+    </>
+  );
 };
 
-const StepTwoForm: React.ForwardRefRenderFunction<
-  {
-    getData: () => Partial<IForm> | null;
-  },
-  { wizardData: any[] }
-> = (props, ref) => {
+const StepTwoForm: IStepForm<IForm> = (props, ref) => {
   const [showSideBar, setShowSideBar] = useState(false);
-  const [formData, setFormData] = useState<Partial<IForm>>({});
-  const [, setFormError] = useState<any>({});
-  const { data: tableNames } = useTableNames(formData.dataSource);
-  // const loading = !tableNames && !fetchError;
+  const { formProps } = useWizard(ref);
+  const { watch, resetField, setValue } = formProps;
+  const dataSource = watch('dataSource2');
+  const tableName = watch('detail2.tableName');
 
-  useImperativeHandle(ref, () => {
-    return {
-      getData: () => {
-        const err = checkForm(formData);
-        if (err) {
-          setFormError(err);
-          return null;
-        }
-        return formData;
-      }
-    };
-  });
+  const resetDetail = () => {
+    resetField('detail2');
+    setValue('detail2.tableName', '');
+  };
 
   let detail: JSX.Element | null = null;
-  if (formData.dataSource === 'mysql') {
-    detail = (
-      <div className="form-group">
-        <label className="form-label" htmlFor="step-2-presql">
-          preSQL
-        </label>
-        <textarea
-          className="form-input"
-          id="step-2-fieldname"
-          placeholder="preSQL"
-          value={formData.preSql}
-          onChange={evt =>
-            setFormData({ ...formData, preSql: evt.target.value })
-          }
-        />
-      </div>
-    );
-  } else if (formData.dataSource === 'hive') {
-    detail = (
-      <HiveDetail
-        value={formData}
-        onChange={value => setFormData({ ...formData, ...value })}
-      />
-    );
+  if (dataSource === 'mysql') {
+    detail = <MysqlDetail formProps={formProps} />;
+  } else if (dataSource === 'hive') {
+    detail = <HiveDetail formProps={formProps} />;
   }
 
   return (
-    <form>
-      <div className="form-group">
-        <label
-          className="form-label field-required"
-          htmlFor="step-2-datasource"
-        >
-          选择数据源
-        </label>
-        <select
-          className="form-input"
-          id="step-2-datasource"
-          value={formData.dataSource ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, dataSource: evt.target.value })
-          }
-        >
-          <option value="" />
-          <option value="mysql">MySQL</option>
-          <option value="hive">Hive</option>
-        </select>
-      </div>
-      <div className="form-group">
-        <label className="form-label field-required" htmlFor="step-2-datatable">
-          选择表
-        </label>
-        <select
-          className="form-input"
-          id="step-2-datatable"
-          value={formData.tableName ?? ''}
-          onChange={evt =>
-            setFormData({ ...formData, tableName: evt.target.value })
-          }
-        >
-          <option value="" />
-          {tableNames &&
-            tableNames.map(tn => (
-              <option key={tn} value={tn}>
-                {tn}
-              </option>
-            ))}
-        </select>
-      </div>
+    <form onSubmit={evt => evt.preventDefault()}>
+      <Select<IForm>
+        label="选择数据源"
+        name="dataSource2"
+        options={[
+          { label: 'MySQL', value: 'mysql' },
+          { label: 'Hive', value: 'hive' }
+        ]}
+        required
+        formProps={formProps}
+        changeCallback={resetDetail}
+      />
       {detail}
       <button
         type="button"
@@ -191,8 +133,8 @@ const StepTwoForm: React.ForwardRefRenderFunction<
         表结构预览
       </button>
       <TableShemaDrawer
-        dataSource={formData.dataSource}
-        tableName={formData.tableName}
+        dataSource={dataSource}
+        tableName={tableName}
         show={showSideBar}
         onClose={() => setShowSideBar(false)}
       />
@@ -200,8 +142,4 @@ const StepTwoForm: React.ForwardRefRenderFunction<
   );
 };
 
-export const formConfig = {
-  desc: '步骤2: 配置加载源'
-};
-
 export default forwardRef(StepTwoForm);

+ 0 - 91
packages/jldbq-extenison/src/wizard/SyncWizard.tsx

@@ -1,91 +0,0 @@
-import React, { useRef, useState } from 'react';
-import StepOneForm, { formConfig as formConfig1 } from './StepOneForm';
-import StepTwoForm, { formConfig as formConfig2 } from './StepTwoForm';
-import StepThreeForm, { formConfig as formConfig3 } from './StepThreeForm';
-import StepFourForm, { formConfig as formConfig4 } from './StepFourForm';
-
-const forms = [StepOneForm, StepTwoForm, StepThreeForm, StepFourForm];
-const configs = [formConfig1, formConfig2, formConfig3, formConfig4];
-
-const StepController: React.FunctionComponent<{
-  currentStep: number;
-  onNext: () => void;
-  onPrev: () => void;
-}> = ({ currentStep, onNext, onPrev }) => {
-  return (
-    <div className="step-control-container">
-      <button
-        disabled={currentStep === 0}
-        className="step-control"
-        onClick={onPrev}
-      >
-        上一步
-      </button>
-      <button className="step-control" onClick={onNext}>
-        {currentStep === 3 ? '构建' : '下一步'}
-      </button>
-    </div>
-  );
-};
-
-export const SyncWizard: React.FunctionComponent<{
-  onFinish: (data: any) => void;
-}> = ({ onFinish }) => {
-  const [currentStep, setCurrentStep] = useState(0);
-  const [data, setData] = useState<any[]>([{}, {}, {}, {}]);
-
-  const stepOneRef = useRef<{ getData: () => any }>(null);
-  const stepTwoRef = useRef<{ getData: () => any }>(null);
-  const stepThreeRef = useRef<{ getData: () => any }>(null);
-  const stepFourRef = useRef<{ getData: () => any }>(null);
-
-  const refs = [stepOneRef, stepTwoRef, stepThreeRef, stepFourRef];
-
-  const stepHeaderItems = configs.map(({ desc }, idx) => {
-    return (
-      <li key={idx} className={currentStep === idx ? 'step-active' : ''}>
-        <div className="step-index-item">
-          <div className="step-index-label">{idx + 1}</div>
-        </div>
-        <div className="step-index-desc">{desc}</div>
-      </li>
-    );
-  });
-
-  const steps = forms.map((Form, idx) => (
-    <div
-      key={idx}
-      className={
-        'step-content-form ' + (currentStep === idx ? 'step-active' : '')
-      }
-    >
-      <Form wizardData={data} ref={refs[idx]} />
-    </div>
-  ));
-
-  return (
-    <div className="sync-wizard">
-      <ul className="step-index-list">{stepHeaderItems}</ul>
-      <div className="step-content-container">{steps}</div>
-      <StepController
-        currentStep={currentStep}
-        onPrev={() => setCurrentStep(currentStep - 1)}
-        onNext={() => {
-          const formData = refs[currentStep].current?.getData();
-          if (!formData) {
-            return;
-          }
-          const newData = data.slice();
-          newData[currentStep] = formData;
-          setData(newData);
-
-          if (currentStep === 3) {
-            onFinish(newData);
-          } else {
-            setCurrentStep(currentStep + 1);
-          }
-        }}
-      />
-    </div>
-  );
-};

+ 112 - 0
packages/jldbq-extenison/src/wizard/Wizard.tsx

@@ -0,0 +1,112 @@
+import React, { useState } from 'react';
+import { IStepFormHandle, IWizardData, wizardContext } from './form';
+
+const StepController: React.FunctionComponent<{
+  current: number;
+  total: number;
+  onNext: () => Promise<void>;
+  onPrev: () => void;
+}> = ({ current, total, onNext, onPrev }) => {
+  return (
+    <div className="step-control-container">
+      <button
+        disabled={current === 0}
+        className="step-control"
+        onClick={onPrev}
+      >
+        上一步
+      </button>
+      <button className="step-control" onClick={onNext}>
+        {current === total - 1 ? '构建' : '下一步'}
+      </button>
+    </div>
+  );
+};
+
+interface IStepConf {
+  key: string;
+  title: string;
+}
+
+type IRegister = (
+  config: IStepConf
+) => {
+  key: string;
+  ref: React.RefCallback<IStepFormHandle<any>>;
+};
+
+export const Wizard: React.VoidFunctionComponent<{
+  children: (register: IRegister) => React.ReactElement[];
+  onFinish: (wizardData: IWizardData) => void;
+}> = ({ children, onFinish }) => {
+  const [currentStep, setCurrentStep] = useState(0);
+  const [wizardData, setWizardData] = useState<IWizardData>({});
+
+  const confs: IStepConf[] = [];
+  const refs: { [key: string]: IStepFormHandle<any> | null | undefined } = {};
+
+  const register: IRegister = conf => {
+    confs.push(conf);
+    const { key } = conf;
+    return {
+      key,
+      ref: h => (refs[key] = h)
+    };
+  };
+
+  const forms = children(register);
+  const currentKey = confs[currentStep].key;
+  const totalSteps = confs.length;
+
+  const stepHeaderItems = confs.map(({ key, title }, idx) => {
+    return (
+      <li key={key} className={currentStep === idx ? 'step-active' : ''}>
+        <div className="step-index-item">
+          <div className="step-index-label">{idx + 1}</div>
+        </div>
+        <div className="step-index-desc">{title}</div>
+      </li>
+    );
+  });
+
+  const steps = forms.map((form, idx) => (
+    <div
+      key={confs[idx].key}
+      className={
+        'step-content-form ' + (currentStep === idx ? 'step-active' : '')
+      }
+    >
+      {form}
+    </div>
+  ));
+
+  return (
+    <div className="sync-wizard">
+      <ul className="step-index-list">{stepHeaderItems}</ul>
+      <div className="step-content-container">
+        <wizardContext.Provider value={wizardData}>
+          {steps}
+        </wizardContext.Provider>
+      </div>
+      <StepController
+        current={currentStep}
+        total={totalSteps}
+        onPrev={() => setCurrentStep(currentStep - 1)}
+        onNext={async () => {
+          const formData = await refs[currentKey]?.getData();
+          if (!formData) {
+            return;
+          }
+          const newData = { ...wizardData, [currentKey]: formData };
+          setWizardData(newData);
+
+          if (currentStep === totalSteps - 1) {
+            onFinish(newData);
+          } else {
+            setCurrentStep(currentStep + 1);
+          }
+        }}
+      />
+    </div>
+  );
+};

+ 798 - 0
packages/jldbq-extenison/src/wizard/form.tsx

@@ -0,0 +1,798 @@
+import React, {
+  useContext,
+  useEffect,
+  useImperativeHandle,
+  useRef,
+  useState
+} from 'react';
+import {
+  DragDropContext,
+  Draggable,
+  DragUpdate,
+  Droppable,
+  DropResult
+} from 'react-beautiful-dnd';
+import {
+  Controller,
+  DeepPartial,
+  FieldValues,
+  Path,
+  useForm,
+  UseFormReturn
+} from 'react-hook-form';
+
+export interface IStepFormHandle<T> {
+  getData: () => Promise<T | null>;
+}
+
+export interface IWizardData {
+  [key: string]: any;
+}
+
+export type IStepForm<T> = React.ForwardRefRenderFunction<IStepFormHandle<T>>;
+
+export const wizardContext = React.createContext<IWizardData>({});
+
+interface IUseWizard<T> {
+  wizardData: IWizardData;
+  formProps: UseFormReturn<T>;
+}
+
+export const useWizard = <T,>(
+  ref: React.ForwardedRef<IStepFormHandle<T>>,
+  defaultValues?: DeepPartial<T>
+): IUseWizard<T> => {
+  const wizardData = useContext(wizardContext);
+  const formProps = useForm<T>({ defaultValues });
+  const { trigger, getValues } = formProps;
+
+  useImperativeHandle(ref, () => {
+    return {
+      getData: async () => {
+        if (await trigger()) {
+          return getValues();
+        }
+        return null;
+      }
+    };
+  });
+
+  return { wizardData, formProps };
+};
+
+export const Select = <T extends FieldValues>({
+  label,
+  name,
+  options,
+  required,
+  formProps,
+  changeCallback
+}: {
+  label: string;
+  name: Path<T>;
+  options: { label?: string; value: string }[];
+  required?: boolean;
+  formProps: UseFormReturn<T>;
+  changeCallback?: () => void;
+}): React.ReactElement => {
+  const { register, getFieldState, clearErrors, formState } = formProps;
+  const { error } = getFieldState(name, formState);
+  const fieldProps = register(name, { required });
+  const { onChange } = fieldProps;
+
+  const handleChange = async (evt: React.ChangeEvent<HTMLSelectElement>) => {
+    await onChange(evt);
+    clearErrors(name);
+    if (changeCallback) {
+      changeCallback();
+    }
+  };
+
+  return (
+    <div className="form-group">
+      <label
+        className={'form-label ' + (required ? 'field-required' : '')}
+        htmlFor={name}
+      >
+        {label}
+      </label>
+      <div
+        className={'form-input-wrapper ' + (error ? 'form-input-error' : '')}
+      >
+        <select
+          className="form-input"
+          id={name}
+          {...fieldProps}
+          onChange={handleChange}
+        >
+          <option value="" />
+          {options.map(item => (
+            <option key={item.value} value={item.value}>
+              {item.label || item.value}
+            </option>
+          ))}
+        </select>
+        {error && <div className="form-input-errmsg">该项不能为空</div>}
+      </div>
+    </div>
+  );
+};
+
+export const Input = <T extends FieldValues>({
+  label,
+  name,
+  placeholder,
+  required,
+  formProps
+}: {
+  label: string;
+  name: Path<T>;
+  placeholder?: string;
+  required?: boolean;
+  formProps: UseFormReturn<T>;
+}): React.ReactElement => {
+  const { register, getFieldState, clearErrors, formState } = formProps;
+  const { error } = getFieldState(name, formState);
+  const fieldProps = register(name, { required });
+  const { onChange } = fieldProps;
+
+  const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
+    clearErrors(name);
+    void onChange(evt);
+  };
+
+  return (
+    <div className="form-group">
+      <label
+        className={'form-label ' + (required ? 'field-required' : '')}
+        htmlFor={name}
+      >
+        {label}
+      </label>
+      <div
+        className={'form-input-wrapper ' + (error ? 'form-input-error' : '')}
+      >
+        <input
+          className="form-input"
+          type="text"
+          autoComplete="off"
+          id={name}
+          placeholder={placeholder}
+          {...fieldProps}
+          onChange={handleChange}
+        />
+        {error && <div className="form-input-errmsg">该项不能为空</div>}
+      </div>
+    </div>
+  );
+};
+
+export const Textarea = <T extends FieldValues>({
+  label,
+  name,
+  placeholder,
+  required,
+  formProps
+}: {
+  label: string;
+  name: Path<T>;
+  placeholder?: string;
+  required?: boolean;
+  formProps: UseFormReturn<T>;
+}): React.ReactElement => {
+  const { register, getFieldState, clearErrors, formState } = formProps;
+  const { error } = getFieldState(name, formState);
+  const fieldProps = register(name, { required });
+  const { onChange } = fieldProps;
+
+  const handleChange = (evt: React.ChangeEvent<HTMLTextAreaElement>) => {
+    clearErrors(name);
+    void onChange(evt);
+  };
+
+  return (
+    <div className="form-group">
+      <label
+        className={'form-label ' + (required ? 'field-required' : '')}
+        htmlFor={name}
+      >
+        {label}
+      </label>
+      <div
+        className={'form-input-wrapper ' + (error ? 'form-input-error' : '')}
+      >
+        <textarea
+          className="form-input"
+          autoComplete="off"
+          id={name}
+          placeholder={placeholder}
+          {...fieldProps}
+          onChange={handleChange}
+        />
+        {error && <div className="form-input-errmsg">该项不能为空</div>}
+      </div>
+    </div>
+  );
+};
+
+const NumberInput: React.FunctionComponent<{
+  id?: string;
+  value?: number;
+  onChange: (value: number) => void;
+}> = ({ id, value, onChange }) => {
+  const valStr = value !== undefined ? value.toString() : '';
+  const [draft, setDraft] = useState(valStr);
+  const [isEditing, setIsEditing] = useState(false);
+  const inputRef = useRef<HTMLInputElement>(null);
+
+  // getDerivedProps
+  if (!isEditing && valStr !== draft) {
+    setDraft(valStr);
+  }
+
+  return (
+    <div className="form-input-number">
+      <button type="button" onClick={() => onChange((value || 0) - 1)} />
+      <input
+        className="form-input"
+        type="text"
+        autoComplete="off"
+        ref={inputRef}
+        id={id}
+        value={draft}
+        onFocus={() => {
+          inputRef.current?.select();
+          setIsEditing(true);
+        }}
+        onBlur={() => {
+          const newVal = parseInt(draft, 10);
+          if (!isNaN(newVal)) {
+            onChange(newVal);
+          }
+          setIsEditing(false);
+        }}
+        onChange={evt => setDraft(evt.target.value)}
+      />
+      <button type="button" onClick={() => onChange((value || 0) + 1)} />
+    </div>
+  );
+};
+
+export const FormNumberInput = <T extends FieldValues>({
+  label,
+  name,
+  positive,
+  required,
+  formProps
+}: {
+  label: string;
+  name: Path<T>;
+  positive?: boolean;
+  required?: boolean;
+  formProps: UseFormReturn<T>;
+}): React.ReactElement => {
+  const { clearErrors, control } = formProps;
+
+  return (
+    <Controller
+      control={control}
+      name={name}
+      rules={{ required }}
+      render={({ field: { onChange, value }, fieldState: { error } }) => {
+        return (
+          <div className="form-group">
+            <label
+              className={'form-label ' + (required ? 'field-required' : '')}
+              htmlFor={name}
+            >
+              {label}
+            </label>
+            <div
+              className={
+                'form-input-wrapper ' + (error ? 'form-input-error' : '')
+              }
+            >
+              <NumberInput
+                id={name}
+                onChange={value => {
+                  clearErrors(name);
+                  onChange(positive ? Math.max(value, 0) : value);
+                }}
+                value={value}
+              />
+              {error && <div className="form-input-errmsg">该项不能为空</div>}
+            </div>
+          </div>
+        );
+      }}
+    />
+  );
+};
+
+// 表3
+
+export interface IMapping<T> {
+  source: T;
+  dest: T;
+}
+
+export class PlaceHolderItem {
+  private _id: string;
+
+  get id(): string {
+    return this._id;
+  }
+
+  constructor(id: string) {
+    this._id = id;
+  }
+}
+
+const useList = <T,>(
+  initSourceValue: T[],
+  initDestValue: T[],
+  reset: () => void
+): {
+  sourceList: (T | PlaceHolderItem)[];
+  destList: T[];
+  selection: Map<string, IMapping<T>>;
+  controller: {
+    reorder: (startIndex: number, endIndex: number) => void;
+    remove: (indices: number[]) => void;
+    prepend: (mapping: IMapping<T>[]) => void;
+    addSelection: (newSelection: Map<string, IMapping<T>>) => void;
+    clearSelection: (...rowIds: string[]) => void;
+    clearSelectionAll: () => void;
+  };
+} => {
+  const getInitSourceList = () => {
+    const result: (T | PlaceHolderItem)[] = [...initSourceValue];
+    const lengthDiff = initDestValue.length - initSourceValue.length;
+    if (lengthDiff > 0) {
+      for (let i = 0; i < lengthDiff; i++) {
+        result.push(new PlaceHolderItem(`placeholder-${i}`));
+      }
+    }
+    return result;
+  };
+
+  const [sourceList, setSourceList] = useState(getInitSourceList);
+  const [destList, setDestList] = useState(initDestValue);
+  const [selection, setSelection] = useState(new Map<string, IMapping<T>>());
+
+  useEffect(() => {
+    setSourceList(getInitSourceList());
+    setDestList(initDestValue);
+    setSelection(new Map<string, IMapping<T>>());
+    reset();
+  }, [initSourceValue, initDestValue]);
+
+  const reorder = (startIndex: number, endIndex: number) => {
+    const sourceResult = Array.from(sourceList);
+    const [sourceRemoved] = sourceResult.splice(startIndex, 1);
+    sourceResult.splice(endIndex, 0, sourceRemoved);
+    setSourceList(sourceResult);
+  };
+
+  const remove = (indices: number[]) => {
+    const idx = new Set(indices);
+    const sourceResult = sourceList.filter((v, i) => !idx.has(i));
+    const destResult = destList.filter((v, i) => !idx.has(i));
+    setSourceList(sourceResult);
+    setDestList(destResult);
+  };
+
+  const prepend = (mapping: IMapping<T>[]) => {
+    const sourceResult = mapping.map(v => v.source);
+    const destResult = mapping.map(v => v.dest);
+    setSourceList([...sourceResult, ...sourceList]);
+    setDestList([...destResult, ...destList]);
+  };
+
+  const addSelection = (newSelection: Map<string, IMapping<T>>) => {
+    const newState = new Map(selection);
+    newSelection.forEach((v, k) => newState.set(k, v));
+    setSelection(newState);
+  };
+
+  const clearSelection = (...rowIds: string[]) => {
+    const newSelState = new Map(selection);
+    rowIds.forEach(v => newSelState.delete(v));
+    setSelection(newSelState);
+  };
+
+  const clearSelectionAll = () => {
+    setSelection(new Map());
+  };
+
+  return {
+    sourceList,
+    destList,
+    selection,
+    controller: {
+      reorder,
+      remove,
+      prepend,
+      addSelection,
+      clearSelection,
+      clearSelectionAll
+    }
+  };
+};
+
+const DndSortList = <T,>({
+  initSourceList,
+  initDestList,
+  sourceName,
+  destName,
+  matchFn,
+  createRowId,
+  formatItemName,
+  onChange,
+  errmsg
+}: {
+  initSourceList: T[];
+  initDestList: T[];
+  sourceName: string;
+  destName: string;
+  matchFn: (source?: T | PlaceHolderItem, dest?: T) => boolean;
+  createRowId: (
+    source?: T | PlaceHolderItem,
+    dest?: T,
+    idx?: number
+  ) => { sourceId: string; destId: string; rowId: string };
+  formatItemName: (value: T, isSource: boolean) => string;
+  onChange: (value: IMapping<T>[]) => void;
+  errmsg?: string;
+}): React.ReactElement => {
+  const [mappings, setMappings] = useState<IMapping<T>[]>([]);
+  const { sourceList, destList, selection, controller } = useList(
+    initSourceList,
+    initDestList,
+    () => {
+      setMappings([]);
+      onChange([]);
+    }
+  );
+  const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
+
+  const selectable = new Map<string, IMapping<T>>();
+  const listLength = Math.max(sourceList.length, destList.length);
+  const maxLength = Math.max(initSourceList.length, initDestList.length, 5);
+
+  const onDragEnd = (result: DropResult) => {
+    setDragOverIndex(null);
+    if (!result.destination) {
+      return;
+    }
+    const idx1 = result.source.index;
+    const idx2 = result.destination.index;
+    const source1 = sourceList[idx1];
+    const dest1 = destList[idx1];
+    const source2 = sourceList[idx2];
+    const dest2 = destList[idx2];
+    const { rowId: rowId1 } = createRowId(source1, dest1);
+    const { rowId: rowId2 } = createRowId(source2, dest2);
+    controller.clearSelection(rowId1, rowId2);
+    controller.reorder(idx1, idx2);
+  };
+
+  const onDragUpdate = (result: DragUpdate) => {
+    if (!result.destination) {
+      setDragOverIndex(null);
+    } else {
+      setDragOverIndex(result.destination.index);
+    }
+  };
+
+  const handleCheckbox = (
+    checked: boolean,
+    rowId: string,
+    source: T,
+    dest: T
+  ) => {
+    if (checked) {
+      controller.addSelection(new Map([[rowId, { source, dest }]]));
+    } else {
+      controller.clearSelection(rowId);
+    }
+  };
+
+  const handleSelectAll = () => {
+    if (selectable.size === 0) {
+      controller.clearSelectionAll();
+    } else {
+      controller.addSelection(selectable);
+    }
+  };
+
+  const handleAddData = () => {
+    const indices: number[] = [];
+    for (let i = 0; i < listLength; i++) {
+      const source = sourceList[i];
+      const dest = destList[i];
+      const { rowId } = createRowId(source, dest, i);
+      if (selection.has(rowId)) {
+        indices.push(i);
+      }
+    }
+    controller.remove(indices);
+    const newMappings = [...mappings, ...Array.from(selection.values())];
+    setMappings(newMappings);
+    onChange(newMappings);
+    controller.clearSelectionAll();
+  };
+
+  const handleRemoveMapping = (row: IMapping<T>) => {
+    const newMappings = mappings.filter(mapping => row !== mapping);
+    controller.prepend([row]);
+    setMappings(newMappings);
+    onChange(newMappings);
+  };
+
+  const handleRemoveAllMapping = () => {
+    controller.prepend(mappings);
+    setMappings([]);
+    onChange([]);
+  };
+
+  const checkList: JSX.Element[] = [];
+  const hintList: JSX.Element[] = [];
+  const sourceElemList: JSX.Element[] = [];
+  const destElemList: JSX.Element[] = [];
+
+  for (let i = 0; i < maxLength; i++) {
+    const source = sourceList[i] as T | PlaceHolderItem | undefined;
+    const dest = destList[i] as T | undefined;
+    const isMatch = matchFn(source, dest);
+    const { sourceId, destId, rowId } = createRowId(source, dest, i);
+
+    if (
+      source &&
+      dest &&
+      !(source instanceof PlaceHolderItem) &&
+      !selection.has(rowId) &&
+      isMatch
+    ) {
+      selectable.set(rowId, {
+        source,
+        dest
+      });
+    }
+
+    if (source && !(source instanceof PlaceHolderItem) && dest) {
+      checkList.push(
+        <div key={rowId} className="form-list-row">
+          <label className="form-list-check">
+            <input
+              type="checkbox"
+              disabled={!isMatch}
+              checked={selection.has(rowId)}
+              onChange={evt =>
+                handleCheckbox(evt.target.checked, rowId, source, dest)
+              }
+            />
+          </label>
+        </div>
+      );
+      hintList.push(
+        <div key={rowId} className="form-list-row">
+          <div
+            className={
+              'form-link-separator ' + (isMatch ? '' : 'form-list-item-diabled')
+            }
+          />
+        </div>
+      );
+    } else {
+      checkList.push(
+        <div key={rowId} className="form-list-row">
+          <label className="form-list-check" />
+        </div>
+      );
+      hintList.push(
+        <div key={rowId} className="form-list-row">
+          <div className="form-link-separator form-link-empty" />
+        </div>
+      );
+    }
+    if (source) {
+      if (!(source instanceof PlaceHolderItem)) {
+        sourceElemList.push(
+          <Draggable key={sourceId} draggableId={sourceId} index={i}>
+            {(provided, snapshot) => (
+              <div
+                className="form-list-row"
+                ref={provided.innerRef}
+                {...provided.draggableProps}
+                {...provided.dragHandleProps}
+                tabIndex={-1}
+              >
+                <div className="form-list-item">
+                  <span className="form-list-cell">
+                    {formatItemName(source, true)}
+                  </span>
+                </div>
+              </div>
+            )}
+          </Draggable>
+        );
+      } else {
+        sourceElemList.push(
+          <Draggable key={sourceId} draggableId={sourceId} index={i}>
+            {(provided, snapshot) => (
+              <div
+                className="form-list-row"
+                ref={provided.innerRef}
+                {...provided.draggableProps}
+                tabIndex={-1}
+              >
+                <div {...provided.dragHandleProps} />
+                <div className="form-list-item">
+                  <span className="form-list-cell form-list-cell-empty" />
+                </div>
+              </div>
+            )}
+          </Draggable>
+        );
+      }
+    } else {
+      sourceElemList.push(
+        <div key={sourceId} className="form-list-row">
+          <div className="form-list-item">
+            <span className="form-list-cell form-list-cell-empty" />
+          </div>
+        </div>
+      );
+    }
+    if (dest) {
+      destElemList.push(
+        <div key={destId} className="form-list-row">
+          <div className="form-list-item">
+            <span
+              className={
+                'form-list-cell ' +
+                (i === dragOverIndex ? 'form-list-cell-light' : '')
+              }
+            >
+              {formatItemName(dest, false)}
+            </span>
+          </div>
+        </div>
+      );
+    } else {
+      destElemList.push(
+        <div key={destId} className="form-list-row">
+          <div className="form-list-item">
+            <span className="form-list-cell form-list-cell-empty" />
+          </div>
+        </div>
+      );
+    }
+  }
+
+  const mappingElemList = mappings.map((row, idx) => {
+    const { rowId } = createRowId(row.source, row.dest, idx);
+    const desc1 = formatItemName(row.source, true);
+    const desc2 = formatItemName(row.dest, false);
+    return (
+      <div key={rowId} className="form-list-row">
+        <div className="form-list-item">
+          <span className="form-list-cell">{desc1}</span>
+          <span className="form-link-separator" />
+          <span className="form-list-cell">{desc2}</span>
+        </div>
+        <button
+          type="button"
+          className="form-list-clear"
+          onClick={() => handleRemoveMapping(row)}
+        />
+      </div>
+    );
+  });
+
+  return (
+    <div className="form-group">
+      <div className="form-list">
+        <div className="form-list-header">
+          <button
+            type="button"
+            className="form-list-check"
+            onClick={handleSelectAll}
+          >
+            全选
+          </button>
+          <div className="form-list-item">
+            <span className="form-list-cell">提取源:{sourceName}</span>
+            <span className="form-link-separator" />
+            <span className="form-list-cell">加载源:{destName}</span>
+          </div>
+        </div>
+        <div className="form-list-body">
+          <div className="form-list-checklist">{checkList}</div>
+          <DragDropContext onDragUpdate={onDragUpdate} onDragEnd={onDragEnd}>
+            <Droppable droppableId="form-list-sourcelist">
+              {(provided, snapshot) => (
+                <div
+                  className="form-list-sourcelist"
+                  ref={provided.innerRef}
+                  {...provided.droppableProps}
+                >
+                  {sourceElemList}
+                  {provided.placeholder}
+                </div>
+              )}
+            </Droppable>
+          </DragDropContext>
+          <div className="form-list-hintlist">{hintList}</div>
+          <div className="form-list-destlist">{destElemList}</div>
+        </div>
+      </div>
+      <div className="form-arrow-separator">
+        <button
+          type="button"
+          className="form-list-add-btn"
+          onClick={handleAddData}
+        />
+      </div>
+      <div className={'form-list ' + (errmsg ? 'form-list-error' : '')}>
+        <div className="form-list-header">
+          <div className="form-list-item">
+            <span className="form-list-cell">映射关系:</span>
+          </div>
+          <button
+            type="button"
+            className="form-list-clear"
+            onClick={handleRemoveAllMapping}
+          >
+            清空
+          </button>
+        </div>
+        <div className="form-list-body">{mappingElemList}</div>
+        {errmsg && <div className="form-list-errmsg">{errmsg}</div>}
+      </div>
+    </div>
+  );
+};
+
+export const FormDndSortList = <T, U extends FieldValues>({
+  name,
+  required,
+  formProps,
+  ...otherProps
+}: {
+  initSourceList: T[];
+  initDestList: T[];
+  sourceName: string;
+  destName: string;
+  matchFn: (source?: T, dest?: T) => boolean;
+  createRowId: (
+    source?: T,
+    dest?: T,
+    idx?: number
+  ) => { sourceId: string; destId: string; rowId: string };
+  formatItemName: (value: T, isSource: boolean) => string;
+  name: Path<U>;
+  required?: boolean;
+  formProps: UseFormReturn<U>;
+}): React.ReactElement => {
+  const { clearErrors, control } = formProps;
+
+  return (
+    <Controller
+      control={control}
+      name={name}
+      rules={{ required }}
+      render={({ field: { onChange }, fieldState: { error } }) => (
+        <DndSortList
+          {...otherProps}
+          onChange={value => {
+            clearErrors(name);
+            onChange(value);
+          }}
+          errmsg={error && '该项不能为空'}
+        />
+      )}
+    />
+  );
+};

+ 0 - 1
packages/jldbq-extenison/src/wizard/index.ts

@@ -1 +0,0 @@
-export * from './SyncWizard';

+ 32 - 0
packages/jldbq-extenison/src/wizard/index.tsx

@@ -0,0 +1,32 @@
+import React from 'react';
+import { Wizard } from './Wizard';
+import StepOneForm from './StepOneForm';
+import StepTwoForm from './StepTwoForm';
+import StepThreeForm from './StepThreeForm';
+import StepFourForm from './StepFourForm';
+
+/* eslint-disable react/jsx-key */
+export const SyncWizard: React.FunctionComponent<{
+  onFinish: (data: any) => void;
+}> = ({ onFinish }) => {
+  return (
+    <Wizard onFinish={onFinish}>
+      {register => {
+        return [
+          <StepOneForm
+            {...register({ key: 'step1', title: '步骤1: 配置提取源' })}
+          />,
+          <StepTwoForm
+            {...register({ key: 'step2', title: '步骤2: 配置加载源' })}
+          />,
+          <StepThreeForm
+            {...register({ key: 'step3', title: '步骤3: 配置转换规则' })}
+          />,
+          <StepFourForm
+            {...register({ key: 'step4', title: '步骤4: 设置同步参数' })}
+          />
+        ];
+      }}
+    </Wizard>
+  );
+};

+ 19 - 4
packages/jldbq-extenison/src/wizard/mockData.ts

@@ -1,5 +1,5 @@
-export function tableSchema(): any {
-  return [
+export function tableSchema(ds: string): any {
+  const data = [
     {
       field: '字段1',
       type: 'string',
@@ -34,10 +34,25 @@ export function tableSchema(): any {
       field: '字段7',
       type: 'string',
       length: 120
+    },
+    {
+      field: '字段8',
+      type: 'date',
+      length: 120
     }
   ];
+  if (ds === 'mysql') {
+    return data.slice(0, data.length - 2);
+  } else {
+    return data;
+  }
 }
 
-export function tableNames(): string[] {
-  return ['表1', '表2', '表3', '表4', '表5'];
+export function tableNames(ds: string): string[] {
+  const data = ['表1', '表2', '表3', '表4', '表5'];
+  if (ds === 'mysql') {
+    return data.slice(0, data.length - 2);
+  } else {
+    return data;
+  }
 }

+ 2 - 2
packages/jldbq-extenison/src/wizard/service.ts

@@ -23,7 +23,7 @@ const tableSchemaFetcher = async (key: {
   }
   // sleep 1
   await new Promise(resolve => setTimeout(resolve, 1));
-  return mockData.tableSchema();
+  return mockData.tableSchema(dataSource);
 };
 
 // mock
@@ -35,7 +35,7 @@ const tableNamesFetcher = async (
   }
   // sleep 1
   await new Promise(resolve => setTimeout(resolve, 1));
-  return mockData.tableNames();
+  return mockData.tableNames(dataSource);
 };
 
 export const useTableSchema = (

+ 17 - 0
packages/jldbq-extenison/style/wizard/form.css

@@ -1,3 +1,20 @@
+.sync-wizard .form-input-wrapper {
+  position: relative;
+}
+
+.sync-wizard .form-input-error .form-input {
+  border-color: #ed5555;
+}
+
+.sync-wizard .form-input-errmsg {
+  position: absolute;
+  top: -14px;
+  left: 0;
+  font-size: 12px;
+  line-height: 12px;
+  color: #ed5555;
+}
+
 .sync-wizard .form-input {
   font-size: 12px;
   border-radius: 2px;

+ 18 - 0
packages/jldbq-extenison/style/wizard/list.css

@@ -23,6 +23,7 @@
 }
 
 .sync-wizard .form-list {
+  position: relative;
   border: 1px solid #c8d3e9;
   border-radius: 2px;
   color: #4a4a4a;
@@ -30,6 +31,19 @@
   min-width: 392px;
 }
 
+.sync-wizard .form-list.form-list-error {
+  border-color: #ed5555;
+}
+
+.sync-wizard .form-list-errmsg {
+  position: absolute;
+  top: -14px;
+  left: 0;
+  font-size: 12px;
+  line-height: 12px;
+  color: #ed5555;
+}
+
 .sync-wizard .form-list-header {
   border-bottom: 1px solid #c8d3e9;
 }
@@ -123,6 +137,10 @@
   align-items: center;
 }
 
+.sync-wizard .form-list-destlist .form-list-cell.form-list-cell-light {
+  background-color: #eef5fe;
+}
+
 .sync-wizard .form-list-body .form-list-cell {
   padding: 0 5px 0 15px;
   font-size: 12px;

+ 5 - 0
yarn.lock

@@ -14785,6 +14785,11 @@ react-highlighter@^0.4.3:
     escape-string-regexp "^1.0.5"
     prop-types "^15.6.0"
 
+react-hook-form@^7.34.1:
+  version "7.34.1"
+  resolved "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.34.1.tgz#06cb216daf706bf9ae4969747115afae0d09410d"
+  integrity sha512-tH7TaZgAURMhjzVE2M/EFmxHz2HdaPMAVs9FXTweNW551VlhXSuVcpcYlkiMZf2zHQiTztupVFpBHJFTma+N7w==
+
 react-hotkeys@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-2.0.0.tgz#a7719c7340cbba888b0e9184f806a9ec0ac2c53f"