feat: 隐藏数据中添加 __changeReason 字段并补充文档说明

This commit is contained in:
2betop 2024-09-19 20:11:03 +08:00 committed by lmaomaoz
parent 2f459084ea
commit a97bf77ee6
21 changed files with 320 additions and 48 deletions

View File

@ -456,3 +456,78 @@ url 中的参数会进入顶层数据域,比如下面的例子,可以点击[
"body": "${word}"
}
```
## 隐藏数据
数据中还有以下字段不会被枚举到,但是可以读取:
- `__prev` 修改前的值
- `__changeReason` 修改原因
- `__changeReason.type` 修改原因类型
- `input` 用户输入
- `api` api 接口返回触发
- `formula` 公式计算触发
- `hide` 隐藏属性变化触发
- `init` 表单项初始化触发
- `action` 事件动作触发
- `__super` 数据链的上一级
> `__changeReason` 字段在 amis 6.9.0 版本开始支持
```schema
{
"data": {
"name": "amis"
},
"type": "form",
id: "form_data",
"actions": [
{
type: "button",
label: "接口获取",
actionType: "ajax",
api: {
"method": "get",
url: "/api/mock2/form/saveForm",
mockResponse: {
status: 200,
data: {
name: "amis-demo"
}
}
}
},
{
type: "button",
label: "设置值",
onEvent: {
click: {
actions: [
{
"actionType": "setValue",
"componentId": "form_data",
"args": {
"value": {
"name": "amis-demo2"
}
}
}
]
}
}
},
],
"body": [
{
type: "input-text",
name: "name",
label: "姓名"
},
{
type: "tpl",
tpl: "当前值:${name}<br />修改前的值:${__prev.name}<br />变化原因:${__changeReason|json}"
}
]
}
```

View File

@ -258,7 +258,8 @@ export function HocStoreFactory(renderer: {
...this.formatData(props.data)
}),
(props.updatePristineAfterStoreDataReInit ??
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false,
props.data?.__changeReason
);
}
} else if (
@ -293,7 +294,9 @@ export function HocStoreFactory(renderer: {
))
}),
(props.updatePristineAfterStoreDataReInit ??
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false,
props.data?.__changeReason
);
} else if (props.data && (props.data as any).__super) {
store.initData(
@ -325,13 +328,15 @@ export function HocStoreFactory(renderer: {
))
}),
(props.updatePristineAfterStoreDataReInit ??
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false,
props.data?.__changeReason
);
} else {
store.initData(
createObject(props.scope, props.data),
(props.updatePristineAfterStoreDataReInit ??
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false,
props.data?.__changeReason
);
}
} else if (
@ -358,7 +363,9 @@ export function HocStoreFactory(renderer: {
(props.updatePristineAfterStoreDataReInit ??
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false ||
(store.storeType === 'FormStore' &&
prevProps.store?.storeType === 'CRUDStore')
prevProps.store?.storeType === 'CRUDStore'),
props.data?.__changeReason
);
}
// nextProps.data.__super !== props.data.__super) &&
@ -376,7 +383,9 @@ export function HocStoreFactory(renderer: {
...store.data
}),
(props.updatePristineAfterStoreDataReInit ??
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false
props.dataUpdatedAt !== prevProps.dataUpdatedAt) === false,
props.data?.__changeReason
);
}
}

View File

@ -11,7 +11,8 @@ import {
ClassName,
BaseApiObject,
SchemaExpression,
SchemaClassName
SchemaClassName,
DataChangeReason
} from '../types';
import {filter, evalExpression} from '../utils/tpl';
import getExprProperties from '../utils/filter-schema';
@ -633,7 +634,9 @@ export default class Form extends React.Component<FormProps, object> {
successMessage: fetchSuccess,
errorMessage: fetchFailed,
onSuccess: (json: Payload, data: any) => {
store.setValues(data);
store.setValues(data, undefined, undefined, undefined, {
type: 'api'
});
if (
!isEffectiveApi(initAsyncApi, store.data) ||
@ -968,7 +971,9 @@ export default class Form extends React.Component<FormProps, object> {
setValues(value: any, replace?: boolean) {
const {store} = this.props;
this.flush();
store.setValues(value, undefined, replace);
store.setValues(value, undefined, replace, undefined, {
type: 'action'
});
}
async submit(
@ -1056,13 +1061,23 @@ export default class Form extends React.Component<FormProps, object> {
value: any,
name: string,
submit: boolean,
changePristine = false
changePristine = false,
changeReason?: DataChangeReason
) {
const {store, formLazyChange, persistDataKeys} = this.props;
if (typeof name !== 'string') {
return;
}
store.changeValue(name, value, changePristine);
store.changeValue(
name,
value,
changePristine,
undefined,
undefined,
changeReason || {
type: 'input'
}
);
if (!changePristine || typeof value !== 'undefined') {
(formLazyChange === false ? this.emitChange : this.lazyEmitChange)(
submit
@ -1131,9 +1146,21 @@ export default class Form extends React.Component<FormProps, object> {
}
}
handleBulkChange(values: Object, submit: boolean) {
handleBulkChange(
values: Object,
submit: boolean,
changeReason?: DataChangeReason
) {
const {onChange, store, formLazyChange} = this.props;
store.setValues(values);
store.setValues(
values,
undefined,
undefined,
undefined,
changeReason || {
type: 'input'
}
);
// store.updateData(values);
// store.items.forEach(formItem => {

View File

@ -27,6 +27,7 @@ import {
BaseApiObject,
BaseSchemaWithoutType,
ClassName,
DataChangeReason,
Schema
} from '../types';
import {HocStoreFactory} from '../WithStore';
@ -530,7 +531,8 @@ export interface FormItemProps extends RendererProps {
) => void;
onBulkChange?: (
values: {[propName: string]: any},
submitOnChange?: boolean
submitOnChange?: boolean,
changeReason?: DataChangeReason
) => void;
addHook: (
fn: Function,

View File

@ -28,7 +28,7 @@ import {observer} from 'mobx-react';
import hoistNonReactStatic from 'hoist-non-react-statics';
import {withRootStore} from '../WithRootStore';
import {FormBaseControl, FormItemWrap} from './Item';
import {Api} from '../types';
import {Api, DataChangeReason} from '../types';
import {TableStore} from '../store/table';
import pick from 'lodash/pick';
import {
@ -75,14 +75,19 @@ export interface ControlOutterProps extends RendererProps {
value: any,
name: string,
submit?: boolean,
changePristine?: boolean
changePristine?: boolean,
changeReason?: DataChangeReason
) => void;
formItemDispatchEvent: (type: string, data: any) => void;
formItemRef?: (control: any) => void;
}
export interface ControlProps {
onBulkChange?: (values: Object) => void;
onBulkChange?: (
values: Object,
submitOnChange?: boolean,
changeReason?: DataChangeReason
) => void;
onChange?: (value: any, name: string, submit: boolean) => void;
store: IIRendererStore;
}
@ -535,7 +540,9 @@ export function wrapControl<
formItem.removeSubFormItem(this.model);
this.model.clearValueOnHidden &&
this.model.form?.deleteValueByName(this.model.name);
this.model.form?.deleteValueByName(this.model.name, {
type: 'hide'
});
isAlive(rootStore) && rootStore.removeStore(this.model);
}
@ -691,7 +698,10 @@ export function wrapControl<
);
}
this.model.changeTmpValue(value, 'input');
this.model.changeTmpValue(
value,
type === 'formula' ? 'formulaChanged' : 'input'
);
if (changeImmediately || conrolChangeImmediately || !formInited) {
this.emitChange(submitOnChange);
@ -772,12 +782,44 @@ export function wrapControl<
return;
}
const changeReason: DataChangeReason = {
type: 'input'
};
if (model.changeMotivation === 'formulaChanged') {
changeReason.type = 'formula';
} else if (
model.changeMotivation === 'initialValue' ||
model.changeMotivation === 'formInited' ||
model.changeMotivation === 'defaultValue'
) {
changeReason.type = 'init';
}
if (model.extraName) {
const values = model.splitExtraValue(value);
onChange?.(values[0], model.name);
onChange?.(values[1], model.extraName, submitOnChange === true);
onChange?.(
values[0],
model.name,
undefined,
undefined,
changeReason
);
onChange?.(
values[1],
model.extraName,
submitOnChange === true,
undefined,
changeReason
);
} else {
onChange?.(value, model.name, submitOnChange === true);
onChange?.(
value,
model.name,
submitOnChange === true,
undefined,
changeReason
);
}
this.checkValidate();
}

View File

@ -5,7 +5,13 @@ import toPairs from 'lodash/toPairs';
import pick from 'lodash/pick';
import {ServiceStore} from './service';
import type {IFormItemStore} from './formItem';
import {Api, ApiObject, fetchOptions, Payload} from '../types';
import {
Api,
ApiObject,
DataChangeReason,
fetchOptions,
Payload
} from '../types';
import {ServerError} from '../utils/errors';
import {
getVariable,
@ -168,9 +174,10 @@ export const FormStore = ServiceStore.named('FormStore')
values: object,
tag?: object,
replace?: boolean,
concatFields?: string | string[]
concatFields?: string | string[],
changeReason?: DataChangeReason
) {
self.updateData(values, tag, replace, concatFields);
self.updateData(values, tag, replace, concatFields, changeReason);
// 如果数据域中有数据变化就都reset一下去掉之前残留的验证消息
self.items.forEach(item => {
@ -209,7 +216,8 @@ export const FormStore = ServiceStore.named('FormStore')
name: string,
value: any,
isPristine: boolean = false,
force: boolean = false
force: boolean = false,
changeReason?: DataChangeReason
) {
// 没有变化就不跑了。
const origin = getVariable(self.data, name, false);
@ -257,13 +265,22 @@ export const FormStore = ServiceStore.named('FormStore')
});
}
changeReason &&
Object.isExtensible(data) &&
Object.defineProperty(data, '__changeReason', {
value: changeReason,
enumerable: false,
configurable: false,
writable: false
});
self.data = data;
// 同步 options
syncOptions();
}
function deleteValueByName(name: string) {
function deleteValueByName(name: string, changeReason?: DataChangeReason) {
const prev = self.data;
const data = cloneObject(self.data);
@ -287,6 +304,15 @@ export const FormStore = ServiceStore.named('FormStore')
}
deleteVariable(data, name);
changeReason &&
Object.isExtensible(data) &&
Object.defineProperty(data, '__changeReason', {
value: changeReason,
enumerable: false,
configurable: false,
writable: false
});
self.data = data;
}
@ -374,7 +400,10 @@ export const FormStore = ServiceStore.named('FormStore')
}
: undefined,
!!(api as ApiObject).replaceData,
(api as ApiObject).concatDataFields
(api as ApiObject).concatDataFields,
{
type: 'api'
}
);
}

View File

@ -18,6 +18,7 @@ import {
extractObjectChain,
injectObjectChain
} from '../utils';
import {DataChangeReason} from '../types';
export const iRendererStore = StoreNode.named('iRendererStore')
.props({
@ -66,7 +67,11 @@ export const iRendererStore = StoreNode.named('iRendererStore')
top = value;
},
initData(data: object = {}, skipSetPristine = false) {
initData(
data: object = {},
skipSetPristine = false,
changeReason?: DataChangeReason
) {
self.initedAt = Date.now();
if (self.data.__tag) {
@ -78,6 +83,15 @@ export const iRendererStore = StoreNode.named('iRendererStore')
self.pristineRaw = data;
}
changeReason &&
Object.isExtensible(data) &&
Object.defineProperty(data, '__changeReason', {
value: changeReason,
enumerable: false,
configurable: false,
writable: false
});
self.data = data;
self.upStreamData = data;
},
@ -90,7 +104,8 @@ export const iRendererStore = StoreNode.named('iRendererStore')
data: object = {},
tag?: object,
replace?: boolean,
concatFields?: string | string[]
concatFields?: string | string[],
changeReason?: DataChangeReason
) {
if (concatFields) {
data = concatData(data, self.data, concatFields);
@ -118,6 +133,15 @@ export const iRendererStore = StoreNode.named('iRendererStore')
writable: false
});
changeReason &&
Object.isExtensible(newData) &&
Object.defineProperty(newData, '__changeReason', {
value: changeReason,
enumerable: false,
configurable: false,
writable: false
});
self.data = newData;
},
@ -126,7 +150,8 @@ export const iRendererStore = StoreNode.named('iRendererStore')
value: any,
changePristine?: boolean,
force?: boolean,
otherModifier?: (data: Object) => void
otherModifier?: (data: Object) => void,
changeReason?: DataChangeReason
) {
if (!name) {
return;
@ -183,6 +208,15 @@ export const iRendererStore = StoreNode.named('iRendererStore')
});
}
changeReason &&
Object.isExtensible(data) &&
Object.defineProperty(data, '__changeReason', {
value: changeReason,
enumerable: false,
configurable: false,
writable: false
});
self.data = data;
},

View File

@ -202,7 +202,10 @@ export const ServiceStore = iRendererStore
normalizeApiResponseData(json.data),
undefined,
!!(api as ApiObject).replaceData,
(api as ApiObject).concatDataFields
(api as ApiObject).concatDataFields,
{
type: 'api'
}
);
self.hasRemoteData = true;
@ -301,7 +304,10 @@ export const ServiceStore = iRendererStore
normalizeApiResponseData(json.data),
undefined,
!!(api as ApiObject).replaceData,
(api as ApiObject).concatDataFields
(api as ApiObject).concatDataFields,
{
type: 'api'
}
);
}
@ -471,7 +477,10 @@ export const ServiceStore = iRendererStore
json.data.data,
undefined,
!!(api as ApiObject).replaceData,
(api as ApiObject).concatDataFields
(api as ApiObject).concatDataFields,
{
type: 'api'
}
);
}

View File

@ -405,9 +405,38 @@ export interface PlainObject {
[propsName: string]: any;
}
export interface DataChangeReason {
type:
| 'input' // 用户输入
| 'api' // api 接口返回触发
| 'formula' // 公式计算触发
| 'hide' // 隐藏属性变化触发
| 'init' // 表单项初始化触发
| 'action'; // 事件动作触发
// 变化的字段名
// 如果是整体变化,那么是 undefined
name?: string;
// 变化的值
value?: any;
}
export interface RendererData {
[propsName: string]: any;
/**
*
*/
__prev?: RendererDataAlias;
/**
*
*/
__changeReason?: DataChangeReason;
/**
*
*/
__super?: RendererData;
}
type RendererDataAlias = RendererData;

View File

@ -18,7 +18,7 @@ import type {RendererConfig, Schema} from 'amis-core';
import type {MenuDivider, MenuItem} from 'amis-ui/lib/components/ContextMenu';
import type {BaseSchema, SchemaCollection} from 'amis';
import type {AsyncLayerOptions} from './component/AsyncLayer';
import type {SchemaType} from 'packages/amis/src/Schema';
import type {SchemaType} from 'amis/lib/Schema';
/**
*

View File

@ -25,7 +25,7 @@ import {
addModal,
mergeDefinitions,
getModals
} from '../../src/util';
} from '../util';
import {
InsertEventContext,
PluginEvent,
@ -60,8 +60,8 @@ import {EditorNode, EditorNodeType} from './node';
import findIndex from 'lodash/findIndex';
import {matchSorter} from 'match-sorter';
import debounce from 'lodash/debounce';
import type {DialogSchema} from '../../../amis/src/renderers/Dialog';
import type {DrawerSchema} from '../../../amis/src/renderers/Drawer';
import type {DialogSchema} from 'amis/lib/renderers/Dialog';
import type {DrawerSchema} from 'amis/lib/renderers/Drawer';
import getLayoutInstance from '../layout';
export interface SchemaHistory {

View File

@ -23,8 +23,8 @@ import merge from 'lodash/merge';
import {EditorModalBody} from './store/editor';
import {filter} from 'lodash';
import type {SchemaType} from 'amis/lib/Schema';
import type {DialogSchema} from '../../amis/src/renderers/Dialog';
import type {DrawerSchema} from '../../amis/src/renderers/Drawer';
import type {DialogSchema} from 'amis/lib/renderers/Dialog';
import type {DrawerSchema} from 'amis/lib/renderers/Drawer';
const {
guid,

View File

@ -21,7 +21,7 @@ import {
import {getEventControlConfig} from '../renderer/event-control/helper';
import omit from 'lodash/omit';
import type {RendererConfig, Schema} from 'amis-core';
import {ModalProps} from 'amis-ui/src/components/Modal';
import {ModalProps} from 'amis-ui/lib/components/Modal';
import ModalSettingPanel from '../component/ModalSettingPanel';
import find from 'lodash/find';

View File

@ -27,7 +27,7 @@ import {getVariables, getQuickVariables, utils} from 'amis-editor-core';
import type {BaseEventContext} from 'amis-editor-core';
import type {VariableItem, FuncGroup} from 'amis-ui';
import {SchemaType} from 'packages/amis/src/Schema';
import {SchemaType} from 'amis/lib/Schema';
export enum FormulaDateType {
NotDate, // 不是时间类

View File

@ -10,7 +10,7 @@ import {autobind, getSchemaTpl} from 'amis-editor-core';
import type {FormControlProps} from 'amis-core';
import type {SchemaCollection} from 'amis';
import type {FormSchema} from '../../../amis/src/Schema';
import type {FormSchema} from 'amis/lib/Schema';
export interface StatusControlProps extends FormControlProps {
name: string;

View File

@ -23,7 +23,7 @@ import {
Select,
Switch
} from 'amis-ui';
import type {EditorModalBody} from '../../../../amis-editor-core/src/store/editor';
import type {EditorModalBody} from 'amis-editor-core/lib/store/editor';
export interface DialogActionPanelProps extends RendererProps {
manager: EditorManager;

View File

@ -47,7 +47,7 @@ import {
} from 'amis-editor-core';
export * from './helper';
import {i18n as _i18n} from 'i18n-runtime';
import type {VariableItem} from 'amis-ui/src/components/formula/CodeEditor';
import type {VariableItem} from 'amis-ui/lib/components/formula/CodeEditor';
import {reaction} from 'mobx';
import {updateComponentContext} from 'amis-editor-core';

View File

@ -10,7 +10,7 @@ import type {SchemaObject} from 'amis';
import flatten from 'lodash/flatten';
import {InputComponentName} from '../component/InputComponentName';
import {FormulaDateType} from '../renderer/FormulaControl';
import type {VariableItem} from 'amis-ui/src/components/formula/CodeEditor';
import type {VariableItem} from 'amis-ui/lib/components/formula/CodeEditor';
import reduce from 'lodash/reduce';
import map from 'lodash/map';
import omit from 'lodash/omit';

View File

@ -307,7 +307,7 @@ test('Picker filter2', async () => {
fireEvent.click(pickerBtn);
await wait(500);
await wait(1000);
const a = container.querySelector('input[name="a"]')!;
const b = container.querySelector('input[name="b"]')!;

View File

@ -543,7 +543,9 @@ export default class Service extends React.Component<ServiceProps> {
);
if (!isEmpty(data) && onBulkChange && formStore) {
onBulkChange(data);
onBulkChange(data, false, {
type: 'api'
});
}
result?.ok && this.initInterval(data);

View File

@ -1,3 +1,4 @@
import {DataChangeReason} from 'amis-core';
import type {ActionSchema} from './renderers/Action';
import {SchemaApi, SchemaApiObject} from './Schema';
@ -156,7 +157,20 @@ export interface PlainObject {
export interface RendererData {
[propsName: string]: any;
/**
*
*/
__prev?: RendererDataAlias;
/**
*
*/
__changeReason?: DataChangeReason;
/**
*
*/
__super?: RendererData;
}
type RendererDataAlias = RendererData;