fix(plugin-formula): expose formula field result in form (#1534)

* fix(plugin-formula): make formula field result available in form

* fix(plugin-formula): fix infinite field change loop
This commit is contained in:
Junyi 2023-03-05 14:48:32 +08:00 committed by GitHub
parent d1fb3c92d8
commit d380c94c52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 211 additions and 228 deletions

View File

@ -69,12 +69,12 @@ export default {
uiSchema: { uiSchema: {
type: 'number', type: 'number',
// title, // title,
'x-disabled': true,
'x-component': 'Formula.Result', 'x-component': 'Formula.Result',
'x-component-props': { 'x-component-props': {
stringMode: true, stringMode: true,
step: '1', step: '1',
}, },
'x-read-pretty': true
}, },
}, },
properties: { properties: {
@ -162,7 +162,6 @@ export default {
'select', 'select',
'multipleSelect', 'multipleSelect',
'snapshot'
// 'json' // 'json'
], ],
useCurrentFields: '{{ useCurrentFields }}', useCurrentFields: '{{ useCurrentFields }}',

View File

@ -1,11 +1,14 @@
import React from 'react';
import { onFormValuesChange } from '@formily/core'; import { onFormValuesChange } from '@formily/core';
import { useField, useFieldSchema, useForm, useFormEffects } from '@formily/react'; import { useFieldSchema, useFormEffects } from '@formily/react';
import cloneDeep from 'lodash/cloneDeep';
import evaluators, { Evaluator } from '@nocobase/evaluators/client'; import evaluators, { Evaluator } from '@nocobase/evaluators/client';
import { Registry, toFixedByStep } from '@nocobase/utils/client'; import { Registry, toFixedByStep } from '@nocobase/utils/client';
import cloneDeep from 'lodash/cloneDeep';
import React, { useState } from 'react';
import { Checkbox, DatePicker, Input as InputString, InputNumber, useCollection } from '@nocobase/client'; import { Checkbox, DatePicker, Input as InputString, InputNumber, useCollection } from '@nocobase/client';
import { toDbType } from '../../utils';
const TypedComponents = { const TypedComponents = {
boolean: Checkbox, boolean: Checkbox,
integer: InputNumber, integer: InputNumber,
@ -16,45 +19,32 @@ const TypedComponents = {
string: InputString, string: InputString,
}; };
const ReadPretty = (props) => { export const Result = (props) => {
const { dataType } = props?.options?? {}; const { value, ...others } = props;
const Component = TypedComponents[dataType] ?? InputString;
return <Component {...props} />;
};
const Input = (props) => {
const { options } = props;
const { dataType, expression, engine = 'math.js' } = options||{};
const { evaluate } = (evaluators as Registry<Evaluator>).get(engine);
const form = useForm();
const val = () => {
const scope = cloneDeep(form.values);
try {
return evaluate(expression, scope);
} catch (error) {
return null;
}
};
const [value, setVal] = useState(() => {
return val();
});
useFormEffects(() => {
onFormValuesChange(() => {
setVal(val());
});
});
if (dataType === 'double') {
return <div>{toFixedByStep(value, props.step)}</div>;
}
return <div>{value}</div>;
};
export const Result = (props: any) => {
const field = useField();
const { getField } = useCollection(); const { getField } = useCollection();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const options = getField(fieldSchema.name as string); const options = getField(fieldSchema.name as string);
return field.readPretty ? <ReadPretty {...props} options={options} /> : <Input {...props} options={options} />; const { dataType, expression, engine = 'math.js' } = options || {};
const { evaluate } = (evaluators as Registry<Evaluator>).get(engine);
useFormEffects(() => {
onFormValuesChange((form) => {
const scope = cloneDeep(form.values);
let v;
try {
v = evaluate(expression, scope);
v = toDbType(v, dataType);
} catch (error) {
v = null;
}
if ((v == null && value == null) || JSON.stringify(v) === JSON.stringify(value)) {
return;
}
// console.log(options.name, v, value, props.defaultValue);
form.setValuesIn(options.name, v);
});
});
const Component = TypedComponents[dataType] ?? InputString;
return <Component {...others} value={dataType === 'double' ? toFixedByStep(value, props.step) : value} />;
}; };
export default Result; export default Result;

View File

@ -1,9 +1,11 @@
import { connect, mapReadPretty } from '@formily/react';
import Expression from './Expression'; import Expression from './Expression';
import Result from './Result'; import Result from './Result';
export const Formula = () => null; export const Formula = () => null;
Formula.Result = Result;
Formula.Expression = Expression; Formula.Expression = Expression;
Formula.Result = connect(Result);
export default Formula; export default Formula;

View File

@ -3,6 +3,8 @@ import { DataTypes } from 'sequelize';
import { BaseFieldOptions, Field } from '@nocobase/database'; import { BaseFieldOptions, Field } from '@nocobase/database';
import evaluators from '@nocobase/evaluators'; import evaluators from '@nocobase/evaluators';
import { DataTypeTransformers, toDbType } from '../utils';
export interface FormulaFieldOptions extends BaseFieldOptions { export interface FormulaFieldOptions extends BaseFieldOptions {
type: 'formula', type: 'formula',
engine: string; engine: string;
@ -10,199 +12,21 @@ export interface FormulaFieldOptions extends BaseFieldOptions {
} }
const DataTypeMap = { const DataTypeMap = {
boolean: { boolean: DataTypes.BOOLEAN,
type: DataTypes.BOOLEAN, integer: DataTypes.INTEGER,
transformers: Boolean bigInt: DataTypes.BIGINT,
}, double: DataTypes.DOUBLE,
integer: { decimal: DataTypes.DECIMAL,
type: DataTypes.INTEGER, string: DataTypes.STRING,
transformers: { date: DataTypes.DATE(3),
boolean(value: boolean) {
return Number(value);
},
number(value: number) {
return value >= 0 ? Math.floor(value) : Math.ceil(value);
},
bigint(value: bigint) {
return Number(value);
},
string(value: string) {
const result = Number.parseInt(value, 10);
if (Number.isNaN(result) || !Number.isFinite(result)) {
return null;
}
return result;
},
date(value: Date) {
const result = value.valueOf();
if (Number.isNaN(result)) {
return null;
}
return result;
}
}
},
bigInt: {
type: DataTypes.BIGINT,
transformers: {
boolean(value: boolean) {
return Number(value);
},
number(value: number) {
return Math.floor(value >= 0 ? Math.floor(value) : Math.ceil(value));
},
bigint(value: bigint) {
return Number(value);
},
string(value: string) {
const result = Number.parseInt(value, 10);
if (Number.isNaN(result) || !Number.isFinite(result)) {
return null;
}
return result;
},
date(value: Date) {
const result = value.valueOf();
if (Number.isNaN(result)) {
return null;
}
return result;
}
}
},
double: {
type: DataTypes.DOUBLE,
transformers: {
boolean(value: boolean) {
return Number(value);
},
number(value: number) {
return value;
},
bigint(value: bigint) {
return Number(value);
},
string(value: string) {
const result = Number.parseFloat(value);
if (Number.isNaN(result) || !Number.isFinite(result)) {
return null;
}
return result;
},
date(value: Date) {
const result = value.valueOf();
if (Number.isNaN(result)) {
return null;
}
return result;
}
}
},
decimal: {
type: DataTypes.DECIMAL,
transformers: {
boolean(value: boolean) {
return Number(value);
},
number(value: number) {
return value;
},
bigint(value: bigint) {
return value;
},
date(value: Date) {
const result = value.valueOf();
if (Number.isNaN(result)) {
return null;
}
return result;
}
}
},
string: {
type: DataTypes.STRING,
transformers: {
boolean(value: boolean) {
return value.toString();
},
number(value: number) {
return value.toString();
},
bigint(value: bigint) {
return value.toString();
},
string(value: string) {
return value;
},
date(value: Date) {
return value.toISOString();
}
}
},
date: {
type: DataTypes.DATE(3),
transformers: {
boolean(value: boolean) {
return null;
},
number(value: number) {
const result = new Date(value);
if (Number.isNaN(result.valueOf())) {
return null;
}
return result;
},
bigint(value: bigint) {
const result = new Date(Number(value));
if (Number.isNaN(result.valueOf())) {
return null;
}
return result;
},
string(value: string) {
const ts = Date.parse(value);
if (Number.isNaN(ts)) {
return null;
}
return new Date(ts);
},
date(value: Date) {
return new Date(value);
}
}
},
} }
function toDbType(value, type) {
if (value == null) {
return null;
}
let jsType: string = typeof value;
if (jsType == 'object' && value instanceof Date) {
jsType = 'date';
}
if (!DataTypeMap[type]) {
return null;
}
const { transformers } = DataTypeMap[type];
if (typeof transformers === 'function') {
return transformers(value);
}
const transformer = transformers[jsType];
return transformer ? transformer(value) : null;
}
export class FormulaField extends Field { export class FormulaField extends Field {
get dataType() { get dataType() {
const { dataType } = this.options; const { dataType } = this.options;
return DataTypeMap[dataType]?.type ?? DataTypes.DOUBLE; return DataTypeMap[dataType] ?? DataTypes.DOUBLE;
} }
calculate(scope) { calculate(scope) {

View File

@ -0,0 +1,168 @@
export const DataTypeTransformers = {
boolean: Boolean,
integer: {
boolean(value: boolean) {
return Number(value);
},
number(value: number) {
return value >= 0 ? Math.floor(value) : Math.ceil(value);
},
bigint(value: bigint) {
return Number(value);
},
string(value: string) {
const result = Number.parseInt(value, 10);
if (Number.isNaN(result) || !Number.isFinite(result)) {
return null;
}
return result;
},
date(value: Date) {
const result = value.valueOf();
if (Number.isNaN(result)) {
return null;
}
return result;
}
},
bigInt: {
boolean(value: boolean) {
return Number(value);
},
number(value: number) {
return Math.floor(value >= 0 ? Math.floor(value) : Math.ceil(value));
},
bigint(value: bigint) {
return Number(value);
},
string(value: string) {
const result = Number.parseInt(value, 10);
if (Number.isNaN(result) || !Number.isFinite(result)) {
return null;
}
return result;
},
date(value: Date) {
const result = value.valueOf();
if (Number.isNaN(result)) {
return null;
}
return result;
}
},
double: {
boolean(value: boolean) {
return Number(value);
},
number(value: number) {
return value;
},
bigint(value: bigint) {
return Number(value);
},
string(value: string) {
const result = Number.parseFloat(value);
if (Number.isNaN(result) || !Number.isFinite(result)) {
return null;
}
return result;
},
date(value: Date) {
const result = value.valueOf();
if (Number.isNaN(result)) {
return null;
}
return result;
}
},
decimal: {
boolean(value: boolean) {
return Number(value);
},
number(value: number) {
return value;
},
bigint(value: bigint) {
return value;
},
date(value: Date) {
const result = value.valueOf();
if (Number.isNaN(result)) {
return null;
}
return result;
}
},
string: {
boolean(value: boolean) {
return value.toString();
},
number(value: number) {
return value.toString();
},
bigint(value: bigint) {
return value.toString();
},
string(value: string) {
return value;
},
date(value: Date) {
return value.toISOString();
}
},
date: {
boolean(value: boolean) {
return null;
},
number(value: number) {
const result = new Date(value);
if (Number.isNaN(result.valueOf())) {
return null;
}
return result;
},
bigint(value: bigint) {
const result = new Date(Number(value));
if (Number.isNaN(result.valueOf())) {
return null;
}
return result;
},
string(value: string) {
const ts = Date.parse(value);
if (Number.isNaN(ts)) {
return null;
}
return new Date(ts);
},
date(value: Date) {
return new Date(value);
}
}
};
export function toDbType(value: any, type: string) {
if (value == null) {
return null;
}
let jsType: string = typeof value;
if (jsType == 'object' && value instanceof Date) {
jsType = 'date';
}
const transformers = DataTypeTransformers[type];
if (!transformers) {
return null;
}
if (typeof transformers === 'function') {
return transformers(value);
}
const transformer = transformers[jsType];
return transformer ? transformer(value) : null;
}