|
@@ -1,24 +1,94 @@
|
|
|
-import React, { forwardRef, useImperativeHandle, useState } from 'react';
|
|
|
-// import { useTableSchema } from './service';
|
|
|
+import React, {
|
|
|
+ forwardRef,
|
|
|
+ useEffect,
|
|
|
+ useImperativeHandle,
|
|
|
+ useState
|
|
|
+} from 'react';
|
|
|
+import {
|
|
|
+ DragDropContext,
|
|
|
+ Draggable,
|
|
|
+ Droppable,
|
|
|
+ DropResult
|
|
|
+} from 'react-beautiful-dnd';
|
|
|
+import { ITableSchemaResponse, useTableSchema } from './service';
|
|
|
|
|
|
+interface IMapping {
|
|
|
+ sourceSchema: ITableSchemaResponse;
|
|
|
+ destSchema: ITableSchemaResponse;
|
|
|
+}
|
|
|
+
|
|
|
+interface IForm {
|
|
|
+ mapping: IMapping[];
|
|
|
+ mode: string;
|
|
|
+}
|
|
|
+
|
|
|
+// TODO:
|
|
|
const checkForm = (formData: any): any => {
|
|
|
return null;
|
|
|
};
|
|
|
|
|
|
+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) {
|
|
|
+ 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];
|
|
|
+};
|
|
|
+
|
|
|
const StepThreeForm: React.ForwardRefRenderFunction<
|
|
|
{
|
|
|
- getData: () => any;
|
|
|
+ getData: () => Partial<IForm> | null;
|
|
|
},
|
|
|
{ wizardData: any[] }
|
|
|
> = ({ wizardData }, ref) => {
|
|
|
- const [formData, setFormData] = useState<any>({});
|
|
|
- // const [selection, setSelection] = useState<string[]>([]);
|
|
|
+ const { dataSource: ds1, tableName: tn1 } = wizardData[0];
|
|
|
+ const { dataSource: ds2, tableName: tn2 } = wizardData[1];
|
|
|
+
|
|
|
+ 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 { dataSource: ds1 } = wizardData[0];
|
|
|
- const { dataSource: ds2 } = wizardData[1];
|
|
|
- // const { data: schema1 } = useTableSchema(ds1, tn1);
|
|
|
- // const { data: schema2 } = useTableSchema(ds2, tn2);
|
|
|
- // const loading = !tableNames && !fetchError;
|
|
|
+
|
|
|
+ const maxListLength = Math.max(size1, size2) || 5;
|
|
|
+ const listLength = Math.max(schema1.length, schema2.length);
|
|
|
|
|
|
useImperativeHandle(ref, () => {
|
|
|
return {
|
|
@@ -33,6 +103,259 @@ const StepThreeForm: React.ForwardRefRenderFunction<
|
|
|
};
|
|
|
});
|
|
|
|
|
|
+ const createRowId = (
|
|
|
+ idx: number,
|
|
|
+ sourceSchema?: ITableSchemaResponse,
|
|
|
+ destSchema?: ITableSchemaResponse
|
|
|
+ ): { sourceId: string; destId: string; rowId: string } => {
|
|
|
+ const sourceId = sourceSchema
|
|
|
+ ? `${ds1}-${tn1}-${sourceSchema.field}`
|
|
|
+ : `${ds1}-${tn1}-source${idx}`;
|
|
|
+ const destId = destSchema
|
|
|
+ ? `${ds2}-${tn2}-${destSchema.field}`
|
|
|
+ : `${ds2}-${tn2}-dest${idx}`;
|
|
|
+ const rowId = `${sourceId}-${destId}`;
|
|
|
+ return { sourceId, destId, rowId };
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatItemName = (
|
|
|
+ tableName: string,
|
|
|
+ schema: ITableSchemaResponse
|
|
|
+ ): 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);
|
|
|
+ };
|
|
|
+
|
|
|
+ const matchField = (
|
|
|
+ sourceSchema?: ITableSchemaResponse,
|
|
|
+ destSchema?: ITableSchemaResponse
|
|
|
+ ): boolean | undefined => {
|
|
|
+ return (
|
|
|
+ sourceSchema &&
|
|
|
+ destSchema &&
|
|
|
+ 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];
|
|
|
+ const destSchema = schema2[i];
|
|
|
+ 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>
|
|
|
<div className="form-group">
|
|
@@ -41,46 +364,44 @@ const StepThreeForm: React.ForwardRefRenderFunction<
|
|
|
<div className="form-group">
|
|
|
<div className="form-list">
|
|
|
<div className="form-list-header">
|
|
|
- <button type="button" className="form-list-check">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className="form-list-check"
|
|
|
+ onClick={handleSelectAll}
|
|
|
+ >
|
|
|
全选
|
|
|
</button>
|
|
|
<div className="form-list-item">
|
|
|
- <span className="form-list-cell">提取源:{ds1}</span>
|
|
|
+ <span className="form-list-cell">提取源:{tn1}</span>
|
|
|
<span className="form-link-separator" />
|
|
|
- <span className="form-list-cell">加载源:{ds2}</span>
|
|
|
+ <span className="form-list-cell">加载源:{tn2}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className="form-list-body">
|
|
|
- <div className="form-list-row">
|
|
|
- <label className="form-list-check">
|
|
|
- <input type="checkbox" />
|
|
|
- </label>
|
|
|
- <div className="form-list-item">
|
|
|
- <span className="form-list-cell">表1字段1.string</span>
|
|
|
- <span className="form-link-separator" />
|
|
|
- <span className="form-list-cell">表2字段1.string</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div className="form-list-row">
|
|
|
- <label className="form-list-check">
|
|
|
- <input type="checkbox" disabled />
|
|
|
- </label>
|
|
|
- <div className="form-list-item form-list-item-diabled">
|
|
|
- <span className="form-list-cell">表1字段2.string</span>
|
|
|
- <span className="form-link-separator" />
|
|
|
- <span className="form-list-cell">表2字段2.string</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div className="form-list-row">
|
|
|
- <div className="form-list-check" />
|
|
|
- </div>
|
|
|
+ <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={() => setFormData({})}
|
|
|
+ onClick={handleAddData}
|
|
|
/>
|
|
|
</div>
|
|
|
<div className="form-list">
|
|
@@ -88,25 +409,19 @@ const StepThreeForm: React.ForwardRefRenderFunction<
|
|
|
<div className="form-list-item">
|
|
|
<span className="form-list-cell">映射关系:</span>
|
|
|
</div>
|
|
|
- <button type="button" className="form-list-clear">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className="form-list-clear"
|
|
|
+ onClick={handleRemoveAllMapping}
|
|
|
+ >
|
|
|
清空
|
|
|
</button>
|
|
|
</div>
|
|
|
- <div className="form-list-body">
|
|
|
- <div className="form-list-row">
|
|
|
- <div className="form-list-item">
|
|
|
- <span className="form-list-cell">表1字段2.string</span>
|
|
|
- <span className="form-link-separator" />
|
|
|
- <span className="form-list-cell">表2字段2.string</span>
|
|
|
- </div>
|
|
|
- <button type="button" className="form-list-clear" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div className="form-list-body">{mappingList}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className="form-group">
|
|
|
- <label className="form-label" htmlFor="step-3-mode">
|
|
|
- <span className="field-required" />
|
|
|
+ <label className="form-label field-required" htmlFor="step-3-mode">
|
|
|
写入模式
|
|
|
</label>
|
|
|
<select
|