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: {
|
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 }}',
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
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