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:
parent
d1fb3c92d8
commit
d380c94c52
@ -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 }}',
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
168
packages/plugins/formula-field/src/utils/index.ts
Normal file
168
packages/plugins/formula-field/src/utils/index.ts
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user