chore: 调整自动填充参照录入逻辑支持默认点选,state 换成 store,优化写入对象时保留原始对象其他字段逻辑 (#10789)

This commit is contained in:
liaoxuezhi 2024-08-16 14:02:31 +08:00 committed by GitHub
parent 422f9ab6c3
commit 0734c2d1bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 241 additions and 153 deletions

View File

@ -1674,6 +1674,8 @@ fillMapping 配置 支持变量取值和表达式;
数据替换并去重combo'${UNIQ(ARRAYMAP(items, item => {platform: item.platform, version: item.version}))}'
数据替换combo: ${items}
`autoFill.defaultSelection` 可以用来配置默认选中
```schema:scope="body"
{
"type": "form",
@ -1685,6 +1687,7 @@ fillMapping 配置 支持变量取值和表达式;
"autoFill": {
"showSuggestion": true,
"api": "/api/mock2/form/autoUpdate?items=1",
"defaultSelection": "${combo}",
"multiple": true,
"fillMapping": {
"combo": "${UNIQ(CONCAT(combo, ARRAYMAP(items, item => {platform: item.platform, version: item.version})))}",

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, {StrictMode} from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';
import {IFormItemStore, IFormStore} from '../store/form';
import {reaction} from 'mobx';
@ -35,7 +35,9 @@ import debounce from 'lodash/debounce';
import {isApiOutdated, isEffectiveApi} from '../utils/api';
import {findDOMNode} from 'react-dom';
import {
createObjectFromChain,
dataMapping,
deleteVariable,
getTreeAncestors,
isEmpty,
keyToPath,
@ -47,6 +49,7 @@ import PopOver from '../components/PopOver';
import CustomStyle from '../components/CustomStyle';
import classNames from 'classnames';
import isPlainObject from 'lodash/isPlainObject';
import {IScopedContext} from '../Scoped';
export type LabelAlign = 'right' | 'left' | 'top' | 'inherit';
@ -407,6 +410,11 @@ export interface FormBaseControl extends BaseSchemaWithoutType {
*/
showSuggestion?: boolean;
/**
*
*/
defaultSelection?: any;
/**
* api
*/
@ -428,7 +436,7 @@ export interface FormBaseControl extends BaseSchemaWithoutType {
/**
* change
*/
trigger?: 'change' | 'foucs';
trigger?: 'change' | 'focus' | 'blur';
/**
*
@ -618,10 +626,6 @@ export class FormItemWrap extends React.Component<FormItemProps> {
constructor(props: FormItemProps) {
super(props);
this.state = {
isOpened: false
};
const {formItem: model, formInited, addHook, initAutoFill} = props;
if (!model) {
return;
@ -632,7 +636,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
() =>
`${model.errors.join('')}${model.isFocused}${
model.dialogOpen
}${JSON.stringify(model.filteredOptions)}`,
}${JSON.stringify(model.filteredOptions)}${model.popOverOpen}`,
() => this.forceUpdate()
)
);
@ -727,19 +731,35 @@ export class FormItemWrap extends React.Component<FormItemProps> {
@autobind
handleBlur(e: any) {
const {formItem: model} = this.props;
const {formItem: model, autoFill} = this.props;
model && model.blur();
this.props.onBlur && this.props.onBlur(e);
if (
!autoFill ||
(autoFill && !autoFill?.hasOwnProperty('showSuggestion'))
) {
return;
}
this.handleAutoFill('blur');
}
handleAutoFill(type: string) {
const {autoFill, onBulkChange, formItem, data} = this.props;
const {autoFill, formItem, data} = this.props;
const {trigger, mode} = autoFill;
if (trigger === type && mode === 'popOver') {
// 参照录入 popOver形式
this.setState({
isOpened: true
});
formItem?.openPopOver(
this.buildAutoFillSchema(),
data,
(confirmed, result) => {
if (!confirmed || !result?.selectedItems) {
return;
}
this.updateAutoFillData(result.selectedItems);
}
);
} else if (
// 参照录入 dialog | drawer
trigger === type &&
@ -749,7 +769,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
this.buildAutoFillSchema(),
data,
(confirmed, result) => {
if (!result?.selectedItems) {
if (!confirmed || !result?.selectedItems) {
return;
}
@ -760,22 +780,22 @@ export class FormItemWrap extends React.Component<FormItemProps> {
}
updateAutoFillData(context: any) {
const {formStore, autoFill, onBulkChange} = this.props;
const {data, autoFill, onBulkChange} = this.props;
const {fillMapping, multiple} = autoFill;
// form原始数据
const data = formStore?.data;
const contextData = createObject(
{items: !multiple ? [context] : context, ...data},
{...context}
);
let responseData: any = {};
responseData = dataMapping(fillMapping, contextData);
const contextData = Array.isArray(context)
? createObject(data, {
items: context
})
: createObjectFromChain([
data,
{
items: [context]
},
context
]);
if (!multiple && !fillMapping) {
responseData = context;
}
onBulkChange?.(responseData);
this.applyMapping(fillMapping ?? {}, contextData, false);
}
syncApiAutoFill = debounce(
@ -808,9 +828,10 @@ export class FormItemWrap extends React.Component<FormItemProps> {
// 自动填充
const itemName = formItem.name;
const ctx = createObject(data, {
[itemName || '']: term,
__term: term
});
setVariable(ctx, itemName, term);
if (
forceLoad ||
(isEffectiveApi(autoFill.api, ctx) && this.lastSearchTerm !== term)
@ -829,20 +850,12 @@ export class FormItemWrap extends React.Component<FormItemProps> {
return;
}
if (autoFill?.fillMapping) {
result = dataMapping(autoFill.fillMapping, result);
}
if (result) {
// 不能把自己给清了吧
setVariable(
result,
itemName,
getVariable(result, itemName) || formItem.tmpValue
);
onBulkChange?.(result);
}
this.applyMapping(
autoFill?.fillMapping ?? {'&': '$$'},
result,
false,
true
);
}
}
} catch (e) {
@ -870,7 +883,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
!isEmpty(autoFill) &&
formItem.filteredOptions.length
) {
const toSync = dataMapping(
this.applyMapping(
autoFill,
multiple
? {
@ -898,46 +911,68 @@ export class FormItemWrap extends React.Component<FormItemProps> {
)
},
selectedOptions[0]
)
),
skipIfExits
);
const tmpData = {...data};
const result = {...toSync};
Object.keys(autoFill).forEach(key => {
const keys = keyToPath(key);
let value = getVariable(toSync, key);
if (skipIfExits) {
const originValue = getVariable(data, key);
if (typeof originValue !== 'undefined') {
value = originValue;
}
}
setVariable(result, key, value);
// 如果左边的 key 是一个路径
// 这里不希望直接把原始对象都给覆盖没了
// 而是保留原始的对象,只修改指定的属性
if (keys.length > 1 && isPlainObject(tmpData[keys[0]])) {
// 存在情况依次更新同一子路径的多个keyeg: a.b.c1 和 a.b.c2所以需要同步更新data
setVariable(tmpData, key, value);
result[keys[0]] = tmpData[keys[0]];
}
});
onBulkChange(result);
}
}
/**
*
*
* @param mapping
* @param ctx
* @param skipIfExits false
*/
applyMapping(
mapping: any,
ctx: any,
skipIfExits = false,
ignoreSelf = false
) {
const {onBulkChange, data, formItem} = this.props;
const toSync = dataMapping(mapping, ctx);
const tmpData = {...data};
const result = {...toSync};
Object.keys(mapping).forEach(key => {
if (key === '&') {
return;
}
const keys = keyToPath(key);
let value = getVariable(toSync, key);
if (skipIfExits) {
const originValue = getVariable(data, key);
if (typeof originValue !== 'undefined') {
value = originValue;
}
}
setVariable(result, key, value);
// 如果左边的 key 是一个路径
// 这里不希望直接把原始对象都给覆盖没了
// 而是保留原始的对象,只修改指定的属性
if (keys.length > 1 && isPlainObject(tmpData[keys[0]])) {
// 存在情况依次更新同一子路径的多个keyeg: a.b.c1 和 a.b.c2所以需要同步更新data
setVariable(tmpData, key, value);
result[keys[0]] = tmpData[keys[0]];
}
});
// 是否忽略自己的设置
if (ignoreSelf && formItem?.name) {
deleteVariable(result, formItem.name);
}
onBulkChange!(result);
}
buildAutoFillSchema() {
const {
render,
autoFill,
classPrefix: ns,
classnames: cx,
translate: __
} = this.props;
const {formItem, autoFill, translate: __} = this.props;
if (!autoFill || (autoFill && !autoFill?.hasOwnProperty('api'))) {
return;
}
@ -947,53 +982,60 @@ export class FormItemWrap extends React.Component<FormItemProps> {
size,
offset,
position,
placement,
multiple,
filter,
columns,
labelField,
popOverContainer,
popOverClassName,
valueField
valueField,
defaultSelection
} = autoFill;
const form = {
type: 'form',
// debug: true,
title: '',
className: 'suggestion-form',
body: {
type: 'picker',
embed: true,
joinValues: false,
label: false,
labelField,
valueField: valueField || 'value',
multiple,
name: 'selectedItems',
options: [],
required: true,
source: api,
pickerSchema: {
type: 'crud',
affixHeader: false,
alwaysShowPagination: true,
keepItemSelectionOnPageChange: true,
headerToolbar: [],
footerToolbar: [
{
type: 'pagination',
align: 'left'
},
{
type: 'bulkActions',
align: 'right',
className: 'ml-2'
}
],
body: [
{
type: 'picker',
embed: true,
joinValues: false,
strictMode: false,
label: false,
labelField,
valueField: valueField || 'value',
multiple,
filter,
columns: columns || []
name: 'selectedItems',
value: defaultSelection || [],
options: [],
required: true,
source: api,
pickerSchema: {
type: 'crud',
bodyClassName: 'mb-0',
affixHeader: false,
alwaysShowPagination: true,
keepItemSelectionOnPageChange: true,
headerToolbar: [],
footerToolbar: [
{
type: 'pagination',
align: 'left'
},
{
type: 'bulkActions',
align: 'right',
className: 'ml-2'
}
],
multiple,
filter,
columns: columns || []
}
}
},
],
actions: [
{
type: 'button',
@ -1010,30 +1052,13 @@ export class FormItemWrap extends React.Component<FormItemProps> {
};
if (mode === 'popOver') {
return (
<Overlay
container={popOverContainer || this.target}
target={() => this.target}
placement={position || 'left-bottom-left-top'}
show
>
<PopOver
classPrefix={ns}
className={cx(`${ns}Autofill-popOver`, popOverClassName)}
style={{
minWidth: this.target ? this.target.offsetWidth : undefined
}}
offset={offset}
onHide={this.handleClose}
overlay
>
{render('popOver-auto-fill-form', form, {
onAction: this.handleAction,
onSubmit: this.handleSubmit
})}
</PopOver>
</Overlay>
);
return {
popOverContainer,
popOverClassName,
placement: placement ?? position,
offset,
body: form
};
} else {
return {
type: mode,
@ -1063,28 +1088,36 @@ export class FormItemWrap extends React.Component<FormItemProps> {
// 参照录入popOver提交
@autobind
handleSubmit(values: any) {
handlePopOverConfirm(values: any) {
const {onBulkChange, autoFill} = this.props;
if (!autoFill || (autoFill && !autoFill?.hasOwnProperty('api'))) {
return;
}
this.updateAutoFillData(values.selectedItems);
this.handleClose();
this.closePopOver();
}
@autobind
handleAction(e: React.UIEvent<any>, action: ActionObject, data: object) {
handlePopOverAction(
e: React.UIEvent<any>,
action: ActionObject,
data: object,
throwErrors: boolean = false,
delegate?: IScopedContext
) {
const {onAction} = this.props;
if (action.actionType === 'cancel') {
this.handleClose();
this.closePopOver();
} else if (onAction) {
// 不识别的丢给上层去处理。
return onAction(e, action, data, throwErrors, delegate);
}
}
@autobind
handleClose() {
this.setState({
isOpened: false
});
closePopOver() {
this.props.formItem?.closePopOver();
}
@autobind
@ -1969,7 +2002,9 @@ export class FormItemWrap extends React.Component<FormItemProps> {
id,
wrapperCustomStyle,
env,
classnames: cx
classnames: cx,
popOverContainer,
data
} = this.props;
const mode = this.props.mode || formMode;
@ -2001,6 +2036,33 @@ export class FormItemWrap extends React.Component<FormItemProps> {
}
)
: null}
{model ? (
<Overlay
container={popOverContainer || this.target}
target={() => this.target}
placement={model.popOverSchema?.placement || 'left-bottom-left-top'}
show={model.popOverOpen}
>
<PopOver
className={cx(
`Autofill-popOver`,
model.popOverSchema?.popOverClassName
)}
style={{
minWidth: this.target ? this.target.offsetWidth : undefined
}}
offset={model.popOverSchema?.offset}
onHide={this.closePopOver}
>
{render('popOver-auto-fill-form', model.popOverSchema?.body, {
// data: model.popOverData,
onAction: this.handlePopOverAction,
onSubmit: this.handlePopOverConfirm
})}
</PopOver>
</Overlay>
) : null}
<CustomStyle
{...this.props}
config={{
@ -2236,8 +2298,6 @@ export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
const controlSize = isRuleSize ? size : defaultSize;
//@ts-ignore
const isOpened = this.state.isOpened;
return (
<>
<Control
@ -2276,7 +2336,6 @@ export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
getItemInputClassName(this.props)
)}
></Control>
{isOpened ? this.buildAutoFillSchema() : null}
</>
);
}

View File

@ -129,7 +129,10 @@ export const FormItemStore = StoreNode.named('FormItemStore')
/** 总条数 */
total: 0
}),
accumulatedOptions: types.optional(types.frozen<Array<any>>(), [])
accumulatedOptions: types.optional(types.frozen<Array<any>>(), []),
popOverOpen: false,
popOverData: types.frozen(),
popOverSchema: types.frozen()
})
.views(self => {
function getForm(): any {
@ -1486,6 +1489,27 @@ export const FormItemStore = StoreNode.named('FormItemStore')
}
}
function openPopOver(
schema: any,
ctx: any,
callback?: (confirmed?: any, value?: any) => void
) {
self.popOverData = ctx || {};
self.popOverOpen = true;
self.popOverSchema = schema;
callback && dialogCallbacks.set(self.popOverData, callback);
}
function closePopOver(confirmed?: any, result?: any) {
const callback = dialogCallbacks.get(self.popOverData);
self.popOverOpen = false;
if (callback) {
dialogCallbacks.delete(self.popOverData);
setTimeout(() => callback(confirmed, result), 200);
}
}
function changeTmpValue(
value: any,
changeReason?:
@ -1560,6 +1584,8 @@ export const FormItemStore = StoreNode.named('FormItemStore')
resetValidationStatus,
openDialog,
closeDialog,
openPopOver,
closePopOver,
changeTmpValue,
changeEmitedValue,
addSubFormItem,

View File

@ -28,7 +28,7 @@ export class TransferPicker extends React.Component<TabsTransferPickerProps> {
}
@autobind
onFoucs() {
onFocus() {
this.props.onFocus?.();
}
@ -60,7 +60,7 @@ export class TransferPicker extends React.Component<TabsTransferPickerProps> {
title={__('Select.placeholder')}
mobileUI={mobileUI}
popOverContainer={popOverContainer}
onFocus={this.onFoucs}
onFocus={this.onFocus}
onClose={this.onBlur}
bodyRender={({onClose, value, onChange, setState, ...states}) => {
return (

View File

@ -45,7 +45,7 @@ export class TransferPicker extends React.Component<
}
@autobind
onFoucs() {
onFocus() {
this.props.onFocus?.();
}
@ -88,7 +88,7 @@ export class TransferPicker extends React.Component<
return (
<PickerContainer
title={__('Select.placeholder')}
onFocus={this.onFoucs}
onFocus={this.onFocus}
onClose={this.onBlur}
mobileUI={mobileUI}
popOverContainer={popOverContainer}

View File

@ -52,7 +52,7 @@ export interface FormulaInputProps
*/
mixedMode?: boolean;
autoFoucs?: boolean;
autoFocus?: boolean;
variables?: VariableItem[];
functions?: Array<FuncGroup>;

View File

@ -228,7 +228,7 @@ export default class SelectControl extends React.Component<SelectProps, any> {
this.input = ref;
}
foucs() {
focus() {
this.input && this.input.focus();
}