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

View File

@ -1,11 +1,14 @@
import React from 'react';
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 { 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 { toDbType } from '../../utils';
const TypedComponents = {
boolean: Checkbox,
integer: InputNumber,
@ -16,45 +19,32 @@ const TypedComponents = {
string: InputString,
};
const ReadPretty = (props) => {
const { dataType } = props?.options?? {};
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();
export const Result = (props) => {
const { value, ...others } = props;
const { getField } = useCollection();
const fieldSchema = useFieldSchema();
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;

View File

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

View File

@ -3,6 +3,8 @@ import { DataTypes } from 'sequelize';
import { BaseFieldOptions, Field } from '@nocobase/database';
import evaluators from '@nocobase/evaluators';
import { DataTypeTransformers, toDbType } from '../utils';
export interface FormulaFieldOptions extends BaseFieldOptions {
type: 'formula',
engine: string;
@ -10,199 +12,21 @@ export interface FormulaFieldOptions extends BaseFieldOptions {
}
const DataTypeMap = {
boolean: {
type: DataTypes.BOOLEAN,
transformers: Boolean
},
integer: {
type: DataTypes.INTEGER,
transformers: {
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);
}
}
},
boolean: DataTypes.BOOLEAN,
integer: DataTypes.INTEGER,
bigInt: DataTypes.BIGINT,
double: DataTypes.DOUBLE,
decimal: DataTypes.DECIMAL,
string: DataTypes.STRING,
date: DataTypes.DATE(3),
}
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 {
get dataType() {
const { dataType } = this.options;
return DataTypeMap[dataType]?.type ?? DataTypes.DOUBLE;
return DataTypeMap[dataType] ?? DataTypes.DOUBLE;
}
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;
}