|
@@ -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 && '该项不能为空'}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ );
|
|
|
+};
|