From 0734c2d1bb6a2537c785eab24a13b0c66d4f1c31 Mon Sep 17 00:00:00 2001 From: liaoxuezhi <2698393+2betop@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:02:31 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E8=B0=83=E6=95=B4=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=A1=AB=E5=85=85=E5=8F=82=E7=85=A7=E5=BD=95=E5=85=A5=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E6=94=AF=E6=8C=81=E9=BB=98=E8=AE=A4=E7=82=B9=E9=80=89?= =?UTF-8?q?,state=20=E6=8D=A2=E6=88=90=20store,=E4=BC=98=E5=8C=96=E5=86=99?= =?UTF-8?q?=E5=85=A5=E5=AF=B9=E8=B1=A1=E6=97=B6=E4=BF=9D=E7=95=99=E5=8E=9F?= =?UTF-8?q?=E5=A7=8B=E5=AF=B9=E8=B1=A1=E5=85=B6=E4=BB=96=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E9=80=BB=E8=BE=91=20(#10789)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/form/formitem.md | 3 + packages/amis-core/src/renderers/Item.tsx | 351 ++++++++++-------- packages/amis-core/src/store/formItem.ts | 28 +- .../src/components/TabsTransferPicker.tsx | 4 +- .../amis-ui/src/components/TransferPicker.tsx | 4 +- .../amis-ui/src/components/formula/Input.tsx | 2 +- packages/amis/src/renderers/Form/Select.tsx | 2 +- 7 files changed, 241 insertions(+), 153 deletions(-) diff --git a/docs/zh-CN/components/form/formitem.md b/docs/zh-CN/components/form/formitem.md index 7af3bcf31..f9b39ce5b 100755 --- a/docs/zh-CN/components/form/formitem.md +++ b/docs/zh-CN/components/form/formitem.md @@ -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})))}", diff --git a/packages/amis-core/src/renderers/Item.tsx b/packages/amis-core/src/renderers/Item.tsx index 4a8472dfa..8846084b2 100644 --- a/packages/amis-core/src/renderers/Item.tsx +++ b/packages/amis-core/src/renderers/Item.tsx @@ -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 { 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 { () => `${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 { @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 { this.buildAutoFillSchema(), data, (confirmed, result) => { - if (!result?.selectedItems) { + if (!confirmed || !result?.selectedItems) { return; } @@ -760,22 +780,22 @@ export class FormItemWrap extends React.Component { } 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 { // 自动填充 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 { 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 { !isEmpty(autoFill) && formItem.filteredOptions.length ) { - const toSync = dataMapping( + this.applyMapping( autoFill, multiple ? { @@ -898,46 +911,68 @@ export class FormItemWrap extends React.Component { ) }, 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]])) { - // 存在情况:依次更新同一子路径的多个key,eg: 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]])) { + // 存在情况:依次更新同一子路径的多个key,eg: 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 { 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 { }; if (mode === 'popOver') { - return ( - this.target} - placement={position || 'left-bottom-left-top'} - show - > - - {render('popOver-auto-fill-form', form, { - onAction: this.handleAction, - onSubmit: this.handleSubmit - })} - - - ); + return { + popOverContainer, + popOverClassName, + placement: placement ?? position, + offset, + body: form + }; } else { return { type: mode, @@ -1063,28 +1088,36 @@ export class FormItemWrap extends React.Component { // 参照录入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, action: ActionObject, data: object) { + handlePopOverAction( + e: React.UIEvent, + 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 { 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 { } ) : null} + + {model ? ( + this.target} + placement={model.popOverSchema?.placement || 'left-bottom-left-top'} + show={model.popOverOpen} + > + + {render('popOver-auto-fill-form', model.popOverSchema?.body, { + // data: model.popOverData, + onAction: this.handlePopOverAction, + onSubmit: this.handlePopOverConfirm + })} + + + ) : null} ) { const controlSize = isRuleSize ? size : defaultSize; - //@ts-ignore - const isOpened = this.state.isOpened; return ( <> ) { getItemInputClassName(this.props) )} > - {isOpened ? this.buildAutoFillSchema() : null} ); } diff --git a/packages/amis-core/src/store/formItem.ts b/packages/amis-core/src/store/formItem.ts index 017a17dd5..accf9339a 100644 --- a/packages/amis-core/src/store/formItem.ts +++ b/packages/amis-core/src/store/formItem.ts @@ -129,7 +129,10 @@ export const FormItemStore = StoreNode.named('FormItemStore') /** 总条数 */ total: 0 }), - accumulatedOptions: types.optional(types.frozen>(), []) + accumulatedOptions: types.optional(types.frozen>(), []), + 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, diff --git a/packages/amis-ui/src/components/TabsTransferPicker.tsx b/packages/amis-ui/src/components/TabsTransferPicker.tsx index 9bcc05c91..46227096b 100644 --- a/packages/amis-ui/src/components/TabsTransferPicker.tsx +++ b/packages/amis-ui/src/components/TabsTransferPicker.tsx @@ -28,7 +28,7 @@ export class TransferPicker extends React.Component { } @autobind - onFoucs() { + onFocus() { this.props.onFocus?.(); } @@ -60,7 +60,7 @@ export class TransferPicker extends React.Component { title={__('Select.placeholder')} mobileUI={mobileUI} popOverContainer={popOverContainer} - onFocus={this.onFoucs} + onFocus={this.onFocus} onClose={this.onBlur} bodyRender={({onClose, value, onChange, setState, ...states}) => { return ( diff --git a/packages/amis-ui/src/components/TransferPicker.tsx b/packages/amis-ui/src/components/TransferPicker.tsx index b851578ab..911349912 100644 --- a/packages/amis-ui/src/components/TransferPicker.tsx +++ b/packages/amis-ui/src/components/TransferPicker.tsx @@ -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 ( ; diff --git a/packages/amis/src/renderers/Form/Select.tsx b/packages/amis/src/renderers/Form/Select.tsx index 20969cfa9..78e052fd3 100644 --- a/packages/amis/src/renderers/Form/Select.tsx +++ b/packages/amis/src/renderers/Form/Select.tsx @@ -228,7 +228,7 @@ export default class SelectControl extends React.Component { this.input = ref; } - foucs() { + focus() { this.input && this.input.focus(); }