Skip to content

Commit

Permalink
feat(table): add new api actionRef.saveEditable
Browse files Browse the repository at this point in the history
  • Loading branch information
shijistar committed Oct 28, 2022
1 parent 662d12c commit cfe2106
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 73 deletions.
222 changes: 154 additions & 68 deletions packages/utils/src/useEditableArray/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ import useMergedState from 'rc-util/lib/hooks/useMergedState';
import get from 'rc-util/lib/utils/get';
import set from 'rc-util/lib/utils/set';
import { noteOnce } from 'rc-util/lib/warning';
import React, { useContext, useMemo, useRef, useState } from 'react';
import React, {
useContext,
useMemo,
useEffect,
useRef,
useState,
forwardRef,
useImperativeHandle,
createRef,
} from 'react';
import { useDebounceFn, useRefFunction } from '..';
import { ProFormContext } from '../components/ProFormContext';
import { useDeepCompareEffectDebounce } from '../hooks/useDeepCompareEffect';
Expand Down Expand Up @@ -276,66 +285,78 @@ export function editableRowByKey<RecordType>(
*
* @param ActionRenderConfig
*/
export function SaveEditableAction<T>({
recordKey,
onSave,
row,
children,
newLineConfig,
editorType,
tableName,
}: ActionRenderConfig<T> & { row: any; children: any }) {
export function SaveEditableAction<T>(
{
recordKey,
onSave,
row,
children,
newLineConfig,
editorType,
tableName,
}: ActionRenderConfig<T> & { row: any; children: any },
ref: React.Ref<SaveEditableActionRef<T>>,
) {
const context = useContext(ProFormContext);
const form = Form.useFormInstance();
const [loading, setLoading] = useMountMergeState<boolean>(false);
const save = useRefFunction(async () => {
try {
const isMapEditor = editorType === 'Map';
// 为了兼容类型为 array 的 dataIndex,当 recordKey 是一个数组时,用于获取表单值的 key 只取第一项,
// 从表单中获取回来之后,再根据 namepath 获取具体的某个字段并设置
const namePath = [tableName, Array.isArray(recordKey) ? recordKey[0] : recordKey]
.map((key) => key?.toString())
.flat(1)
.filter(Boolean) as string[];
setLoading(true);
// @ts-expect-error
await form.validateFields(namePath, {
recursive: true,
});

const fields = context.getFieldFormatValue?.(namePath) || form.getFieldValue(namePath);
// 处理 dataIndex 为数组的情况
if (Array.isArray(recordKey) && recordKey.length > 1) {
// 获取 namepath
const [, ...recordKeyPath] = recordKey;
// 将目标值获取出来并设置到 fields 当中
const curValue = get(fields, recordKeyPath as string[]);
set(fields, recordKeyPath, curValue);
}
const data = isMapEditor ? set({}, namePath, fields, true) : fields;

// 获取数据并保存
const res = await onSave?.(
recordKey,
// 如果是 map 模式,fields 就是一个值,所以需要set 到对象中
// 数据模式 fields 是一个对象,所以不需要
merge({}, row, data),
row,
newLineConfig,
);
setLoading(false);
return res;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
setLoading(false);
throw error;
}
});
useImperativeHandle(ref, () => ({
save,
}));

return (
<a
key="save"
onClick={async (e) => {
e.stopPropagation();
e.preventDefault();
try {
const isMapEditor = editorType === 'Map';
// 为了兼容类型为 array 的 dataIndex,当 recordKey 是一个数组时,用于获取表单值的 key 只取第一项,
// 从表单中获取回来之后,再根据 namepath 获取具体的某个字段并设置
const namePath = [tableName, Array.isArray(recordKey) ? recordKey[0] : recordKey]
.map((key) => key?.toString())
.flat(1)
.filter(Boolean) as string[];
setLoading(true);
// @ts-expect-error
await form.validateFields(namePath, {
recursive: true,
});

const fields = context.getFieldFormatValue?.(namePath) || form.getFieldValue(namePath);
// 处理 dataIndex 为数组的情况
if (Array.isArray(recordKey) && recordKey.length > 1) {
// 获取 namepath
const [, ...recordKeyPath] = recordKey;
// 将目标值获取出来并设置到 fields 当中
const curValue = get(fields, recordKeyPath as string[]);
set(fields, recordKeyPath, curValue);
}
const data = isMapEditor ? set({}, namePath, fields, true) : fields;

// 获取数据并保存
const res = await onSave?.(
recordKey,
// 如果是 map 模式,fields 就是一个值,所以需要set 到对象中
// 数据模式 fields 是一个对象,所以不需要
merge({}, row, data),
row,
newLineConfig,
);
setLoading(false);
return res;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
setLoading(false);
return null;
}
await save();
} catch {}
}}
>
{loading ? (
Expand All @@ -349,6 +370,14 @@ export function SaveEditableAction<T>({
</a>
);
}
export type SaveEditableActionRef<T = any> = {
/**
* 直接触发保存动作
*
* @throws 如果校验失败,会抛出异常
* */
save: () => ReturnType<NonNullable<RowEditableConfig<T>['onSave']>> | Promise<void>;
};

/**
* 删除按钮 dom
Expand Down Expand Up @@ -433,18 +462,24 @@ const CancelEditableAction: React.FC<ActionRenderConfig<any> & { row: any }> = (

export function defaultActionRender<T>(row: T, config: ActionRenderConfig<T, NewLineConfig<T>>) {
const { recordKey, newLineConfig, saveText, deleteText } = config;
const SaveEditableActionRef = forwardRef(SaveEditableAction as typeof SaveEditableAction<T>);
const saveRef = createRef<SaveEditableActionRef<T>>();

return [
<SaveEditableAction<T> key={'save' + recordKey} {...config} row={row}>
{saveText}
</SaveEditableAction>,
newLineConfig?.options.recordKey !== recordKey ? (
<DeleteEditableAction key={'delete' + recordKey} {...config} row={row}>
{deleteText}
</DeleteEditableAction>
) : null,
<CancelEditableAction key={'cancel' + recordKey} {...config} row={row} />,
];
return {
save: (
<SaveEditableActionRef key={'save' + recordKey} {...config} row={row} ref={saveRef}>
{saveText}
</SaveEditableActionRef>
),
saveRef,
delete:
newLineConfig?.options.recordKey !== recordKey ? (
<DeleteEditableAction key={'delete' + recordKey} {...config} row={row}>
{deleteText}
</DeleteEditableAction>
) : undefined,
cancel: <CancelEditableAction key={'cancel' + recordKey} {...config} row={row} />,
};
}

/**
Expand Down Expand Up @@ -647,6 +682,48 @@ export function useEditableArray<RecordType>(
propsOnValuesChange.run(editRow || newLineRecordData, dataSource);
});

const saveRefsMap = useRef<Map<React.Key, React.RefObject<SaveEditableActionRef>>>(
new Map<React.Key, React.RefObject<SaveEditableActionRef>>(),
);
useEffect(() => {
// 确保只保留编辑状态的,其它的都删除掉
saveRefsMap.current.forEach((ref, key) => {
if (!editableKeysSet.has(key)) {
saveRefsMap.current.delete(key);
}
});
}, [saveRefsMap, editableKeysSet]);
/**
* 保存编辑行
*
* @param recordKey
* @param needReTry
*/
const saveEditable = useRefFunction(
async (recordKey: RecordKey, needReTry?: boolean): Promise<boolean> => {
const relayKey = recordKeyToString(recordKey);
const key = dataSourceKeyIndexMapRef.current.get(recordKey.toString());

/** 如果没找到key,转化一下再去找 */
if (!editableKeysSet.has(relayKey) && key && (needReTry ?? true) && props.tableName) {
return await saveEditable(key, false);
}

const saveRef =
saveRefsMap.current.get(relayKey) || saveRefsMap.current.get(relayKey.toString());
try {
await saveRef?.current?.save();
} catch {
return false;
}

editableKeysSet.delete(relayKey);
editableKeysSet.delete(relayKey.toString());
setEditableRowKeys(Array.from(editableKeysSet));
return true;
},
);

/**
* 同时只能支持一行,取消之后数据消息,不会触发 dataSource
*
Expand Down Expand Up @@ -827,15 +904,23 @@ export function useEditableArray<RecordType>(
props.deletePopconfirmMessage || `${intl.getMessage('deleteThisLine', '删除此行')}?`,
};

const defaultDoms = defaultActionRender<RecordType>(row, config);

const renderResult = defaultActionRender<RecordType>(row, config);
// 缓存一下saveRef
if (props.tableName) {
saveRefsMap.current.set(
dataSourceKeyIndexMapRef.current.get(recordKeyToString(key)) || recordKeyToString(key),
renderResult.saveRef,
);
} else {
saveRefsMap.current.set(recordKeyToString(key), renderResult.saveRef);
}
if (props.actionRender)
return props.actionRender(row, config, {
save: defaultDoms[0],
delete: defaultDoms[1],
cancel: defaultDoms[2],
save: renderResult.save,
delete: renderResult.delete,
cancel: renderResult.cancel,
});
return defaultDoms;
return [renderResult.save, renderResult.delete, renderResult.cancel];
};

return {
Expand All @@ -846,6 +931,7 @@ export function useEditableArray<RecordType>(
startEditable,
cancelEditable,
addEditRecord,
saveEditable,
newLineRecord: newLineRecordCache,
preEditableKeys: editableKeysRef,
onValuesChange,
Expand Down
10 changes: 5 additions & 5 deletions packages/utils/src/useEditableMap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,15 @@ export function useEditableMap<RecordType>(
...config,
};

const defaultDoms = defaultActionRender(props.dataSource, renderConfig);
const renderResult = defaultActionRender(props.dataSource, renderConfig);
if (props.actionRender) {
return props.actionRender(props.dataSource, renderConfig, {
save: defaultDoms[0],
delete: defaultDoms[1],
cancel: defaultDoms[2],
save: renderResult.save,
delete: renderResult.delete,
cancel: renderResult.cancel,
});
}
return defaultDoms;
return [renderResult.save, renderResult.delete, renderResult.cancel];
},
[editableKeys && editableKeys.join(','), props.dataSource],
);
Expand Down
69 changes: 69 additions & 0 deletions tests/table/editor-table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,75 @@ describe('EditorProTable', () => {
wrapper.unmount();
});

it('📝 EditableProTable saveEditable should save and quit editing', async () => {
const actionRef = React.createRef<ActionType>();
let changedDataSource: DataSourceType[] = [];
const onChange = jest.fn((value) => {
changedDataSource = value;
});
const wrapper = render(
<ProForm
initialValues={{
table: defaultData,
}}
>
<EditableProTable<DataSourceType>
rowKey="id"
name="table"
onChange={onChange}
actionRef={actionRef}
columns={columns}
/>
</ProForm>,
);
await waitForComponentToPaint(wrapper, 1000);

expect(
wrapper.container.querySelector('.ant-table-tbody')?.querySelectorAll('tr.ant-table-row')
.length,
).toBe(defaultData.length);

const editAndChange = async (inputValue: string) => {
act(() => {
wrapper.container.querySelector<HTMLButtonElement>('#editor')?.click();
});
await waitForComponentToPaint(wrapper, 100);

act(() => {
fireEvent.change(
wrapper.container.querySelectorAll(`.ant-form-item-control-input input`)[1],
{
target: {
value: inputValue,
},
},
);
});
await waitForComponentToPaint(wrapper, 100);
};

await editAndChange('test value');

// 使用recordKey保存
await actionRef.current?.saveEditable(624748504);
await waitForComponentToPaint(wrapper, 100);

expect(onChange).toBeCalled();
expect(changedDataSource).toHaveLength(defaultData.length);
expect(changedDataSource[0]?.title).toBe('test value');

await editAndChange('test value2');
// 设置了name时,还支持使用数组下标保存
await actionRef.current?.saveEditable(0);
await waitForComponentToPaint(wrapper, 100);

expect(onChange).toBeCalled();
expect(changedDataSource).toHaveLength(defaultData.length);
expect(changedDataSource[0]?.title).toBe('test value2');

wrapper.unmount();
});

it('📝 EditableProTable add support children column', async () => {
const onchange = jest.fn();
const wrapper = render(
Expand Down

0 comments on commit cfe2106

Please sign in to comment.