Revert "refactor: formula plugin (#1082)"

This reverts commit 0469b8c44d.
This commit is contained in:
chenos 2022-11-19 18:15:42 +08:00
parent 0469b8c44d
commit 0cbfa0a521
56 changed files with 318 additions and 1021 deletions

View File

@ -1 +0,0 @@
export { default } from '@nocobase/plugin-excel-formula-field/client';

View File

@ -1 +0,0 @@
export { default } from '@nocobase/plugin-math-formula-field/client';

View File

@ -1,5 +1,5 @@
import { Spin } from 'antd';
import React, { useContext, useState } from 'react';
import React, { useState } from 'react';
import { useAPIClient, useRequest } from '../api-client';
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
import { CollectionManagerContext } from './context';
@ -8,13 +8,11 @@ import { CollectionManagerOptions } from './types';
export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => {
const { service, interfaces, collections = [], refreshCM } = props;
const ctx = useContext(CollectionManagerContext);
return (
<CollectionManagerContext.Provider
value={{
...ctx,
service,
interfaces: { ...defaultInterfaces, ...interfaces, ...ctx.interfaces },
interfaces: { ...defaultInterfaces, ...interfaces },
collections,
refreshCM,
}}

View File

@ -1,6 +1,6 @@
import { PlusOutlined } from '@ant-design/icons';
import { ArrayTable } from '@formily/antd';
import { useForm } from '@formily/react';
import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { Button, Dropdown, Menu } from 'antd';
import { cloneDeep } from 'lodash';
@ -14,9 +14,9 @@ import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components';
import { getOptions } from './interfaces';
import { options } from './interfaces';
const getSchema = (schema: IField, record: any, compile) => {
const getSchema = (schema: IField, record: any, compile): ISchema => {
if (!schema) {
return;
}
@ -24,7 +24,7 @@ const getSchema = (schema: IField, record: any, compile) => {
const properties = cloneDeep(schema.properties) as any;
if (schema.hasDefaultValue === true) {
properties['defaultValue'] = cloneDeep(schema?.default?.uiSchema);
properties['defaultValue'] = cloneDeep(schema.default.uiSchema);
properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
properties['defaultValue']['x-decorator'] = 'FormItem';
}
@ -108,9 +108,9 @@ export const useCollectionFieldFormValues = () => {
}
delete values.autoCreateReverseField;
return values;
},
};
};
}
}
}
const useCreateCollectionField = () => {
const form = useForm();
@ -143,7 +143,7 @@ export const AddCollectionField = (props) => {
};
export const AddFieldAction = (props) => {
const { scope, getContainer, item: record, children, trigger, align } = props;
const { scope, getContainer, item: record, children,trigger ,align} = props;
const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
@ -164,13 +164,11 @@ export const AddFieldAction = (props) => {
}}
onClick={(info) => {
const schema = getSchema(getInterface(info.key), record, compile);
if (schema) {
setSchema(schema);
setVisible(true);
}
}}
>
{getOptions().map((option) => {
{options.map((option) => {
return (
option.children.length > 0 && (
<Menu.ItemGroup key={option.label} title={compile(option.label)}>

View File

@ -1,16 +0,0 @@
import { observer } from '@formily/react';
import { Tag } from 'antd';
import React from 'react';
import { useCompile } from '../../../schema-component';
import { useCollectionManager } from '../../hooks';
export const CollectionFieldInterface = observer((props: any) => {
const { value } = props;
const { getInterface } = useCollectionManager();
const compile = useCompile();
const schema = getInterface(value);
if (!schema) return null;
return <Tag>{compile(schema.title)}</Tag>;
});

View File

@ -31,8 +31,7 @@ registerGroupLabel('advanced', '{{t("Advanced type")}}');
registerGroupLabel('systemInfo', '{{t("System info")}}');
registerGroupLabel('others', '{{t("Others")}}');
export const getOptions = () => {
return Object.keys(groupLabels).map((groupName) => {
export const options = Object.keys(groupLabels).map((groupName) => {
return {
label: groupLabels[groupName],
children: Object.keys(fields[groupName] || {})
@ -47,7 +46,4 @@ export const getOptions = () => {
})
.sort((a, b) => a.order - b.order),
};
});
};
export const options = getOptions();
});

View File

@ -1,6 +1,5 @@
import { ISchema } from '@formily/react';
import { CollectionOptions } from '../../types';
import { CollectionFieldInterface } from '../components/CollectionFieldInterface';
import { options } from '../interfaces';
const collection: CollectionOptions = {
@ -156,10 +155,9 @@ export const collectionFieldSchema: ISchema = {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
title: '{{t("Field interface")}}',
properties: {
interface: {
'x-component': CollectionFieldInterface,
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},

View File

@ -106,7 +106,7 @@ export const useCollectionManager = () => {
if (!fieldNames?.length) {
return;
}
let cName: any = collectionName;
let cName = collectionName;
let collectionField;
while (cName && fieldNames.length > 0) {
const fileName = fieldNames.shift();

View File

@ -5,12 +5,10 @@ export * from './CollectionManagerProvider';
export * from './CollectionManagerSchemaComponentProvider';
export * from './CollectionManagerShortcut';
export * from './CollectionProvider';
export * from './Configuration';
export { registerField, registerGroupLabel } from './Configuration/interfaces';
export * from './context';
export * from './hooks';
export * as interfacesProperties from './interfaces/properties';
export * from './interfaces/types';
export * from './ResourceActionProvider';
export * from './types';
export * from './Configuration';

View File

@ -1,22 +1,22 @@
import { IField, interfacesProperties } from '@nocobase/client';
const { defaultProps, operators } = interfacesProperties;
import { defaultProps, operators } from './properties';
import { IField } from './types';
export const mathFormula: IField = {
name: 'mathFormula',
export const formula: IField = {
name: 'formula',
type: 'object',
group: 'advanced',
order: 1,
title: '{{t("Math formula")}}',
description: '{{t("Compute a value based on the other fields using mathjs")}}',
title: '{{t("Formula")}}',
description: '{{t("Formula description")}}',
sortable: true,
default: {
type: 'mathFormula',
type: 'formula',
// name,
uiSchema: {
type: 'number',
// title,
'x-disabled': true,
'x-component': 'MathFormula.Result',
"x-disabled": true,
'x-component': 'Formula.Result',
'x-component-props': {
stringMode: true,
step: '1',
@ -25,16 +25,16 @@ export const mathFormula: IField = {
},
properties: {
...defaultProps,
expression: {
'expression': {
type: 'string',
title: '{{t("Expression")}}',
required: true,
description: '{{t("Input +, -, *, /, ( ) to calculate, input @ to open field variables.")}}',
'x-component': 'MathFormula.Expression',
'x-component': 'Formula.Expression',
'x-decorator': 'FormItem',
'x-component-props': {
supports: ['number', 'percent', 'integer'],
useCurrentFields: '{{ useCurrentFields }}',
'supports': ['number', 'percent', 'integer'],
'useCurrentFields': '{{ useCurrentFields }}'
},
},
'uiSchema.x-component-props.step': {

View File

@ -6,6 +6,8 @@ export * from './createdAt';
export * from './createdBy';
export * from './datetime';
export * from './email';
export * from './formula';
export * from './sequence';
export * from './icon';
export * from './id';
export * from './input';
@ -25,10 +27,8 @@ export * from './phone';
export * from './radioGroup';
export * from './richText';
export * from './select';
export * from './sequence';
export * from './subTable';
export * from './textarea';
export * from './time';
export * from './updatedAt';
export * from './updatedBy';

View File

@ -1,7 +1,8 @@
import { registerValidateFormats } from '@formily/core';
import { i18n } from '../../i18n';
import { defaultProps, operators, unique } from './properties';
import { IField } from './types';
import { i18n } from '../../i18n';
import { registerValidateFormats } from '@formily/core';
import { ISchema } from '@formily/react';
registerValidateFormats({
odd: /^-?\d*[13579]$/,
@ -16,7 +17,7 @@ export const integer: IField = {
title: '{{t("Integer")}}',
sortable: true,
default: {
type: 'bigInt',
type: 'integer',
// name,
uiSchema: {
type: 'number',

View File

@ -1,6 +1,6 @@
import { registerValidateRules } from '@formily/core';
import { defaultProps } from './properties';
import { defaultProps, operators, unique } from './properties';
import { IField } from './types';
import { registerValidateRules } from '@formily/core';
registerValidateRules({
json(value) {
@ -20,7 +20,7 @@ export const json: IField = {
name: 'json',
type: 'object',
group: 'advanced',
order: 4,
order: 3,
title: '{{t("JSON")}}',
sortable: true,
default: {

View File

@ -15,7 +15,6 @@ export const type: ISchema = {
{ label: 'String', value: 'string' },
{ label: 'Text', value: 'text' },
{ label: 'Integer', value: 'integer' },
{ label: 'BigInteger', value: 'bigInt' },
{ label: 'Float', value: 'float' },
{ label: 'Double', value: 'double' },
{ label: 'Decimal', value: 'decimal' },

View File

@ -1,14 +1,14 @@
import { css } from '@emotion/css';
import { ArrayTable, FormButtonGroup, FormDrawer, FormLayout, Submit } from '@formily/antd';
import React, { useContext } from 'react';
import { Button, Select } from 'antd';
import { onFieldValueChange } from '@formily/core';
import { SchemaOptionsContext, useForm, useFormEffects } from '@formily/react';
import { Button, Select } from 'antd';
import React, { useContext } from 'react';
import { ArrayTable, FormButtonGroup, FormDrawer, FormLayout, Submit } from '@formily/antd';
import { useTranslation } from 'react-i18next';
import { css } from '@emotion/css';
import { Cron, SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
import { defaultProps, operators, unique } from './properties';
import { IField } from './types';
import { defaultProps, operators, unique } from './properties';
function RuleTypeSelect(props) {
const compile = useCompile();
@ -252,7 +252,7 @@ export const sequence: IField = {
name: 'sequence',
type: 'object',
group: 'advanced',
order: 3,
order: 2,
title: '{{t("Sequence")}}',
sortable: true,
default: {

View File

@ -1,224 +0,0 @@
import { css } from '@emotion/css';
import { Field, onFormSubmitValidateStart } from '@formily/core';
import { useField, useFormEffects } from '@formily/react';
import { Dropdown, Menu } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
function pasteHtml(html, selectPastedContent = false) {
var sel, range;
if (window.getSelection) {
// IE9 and non-IE
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
// Range.createContextualFragment() would be useful here but is
// only relatively recently standardized and is not supported in
// some browsers (IE9, for one)
var el = document.createElement('div');
el.innerHTML = html;
var frag = document.createDocumentFragment(),
node,
lastNode;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
var firstNode = frag.firstChild;
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
if (selectPastedContent) {
range.setStartBefore(firstNode);
} else {
range.collapse(true);
}
sel.removeAllRanges();
sel.addRange(range);
}
}
}
}
const getValue = (el) => {
const values: any[] = [];
for (const node of el.childNodes) {
if (node.nodeName === 'SPAN') {
values.push(`{{${node['dataset']['key']}}}`);
} else {
values.push(node.textContent?.trim?.());
}
}
const text = values.join(' ')?.replace(/\s+/g, ' ').trim();
return text;
};
const renderExp = (exp: string, scope = {}) => {
return exp.replace(/{{([^}]+)}}/g, (_, i) => {
return scope[i.trim()] || '';
});
};
export const Expression = (props) => {
const { evaluate, value, supports, useCurrentFields } = props;
const field = useField<Field>();
const { t } = useTranslation();
const fields = useCurrentFields();
const inputRef = useRef<any>();
const [changed, setChanged] = useState(false);
const onChange = (value) => {
setChanged(true);
props.onChange(value);
};
const numColumns = new Map<string, string>();
const scope = {};
fields
.filter((field) => supports.includes(field.interface))
.forEach((field) => {
numColumns.set(field.name, field.uiSchema.title);
scope[field.name] = 1;
});
const keys = Array.from(numColumns.keys());
const [html, setHtml] = useState(() => {
const scope = {};
for (const key of keys) {
const val = numColumns.get(key);
scope[
key
] = ` <span class="ant-tag" style="margin: 0 3px;" contentEditable="false" data-key="${key}">${val}</span> `;
}
return renderExp(value || '', scope);
});
useEffect(() => {
if (changed) {
return;
}
const scope = {};
for (const key of keys) {
const val = numColumns.get(key);
scope[
key
] = ` <span class="ant-tag" style="margin: 0 3px;" contentEditable="false" data-key="${key}">${val}</span> `;
}
const val = renderExp(value || '', scope);
setHtml(val);
}, [value]);
const menu = (
<Menu>
{keys.length > 0 ? (
keys.map((key) => (
<Menu.Item disabled key={key}>
<button
onClick={async (args) => {
(inputRef.current as any).focus();
const val = numColumns.get(key);
pasteHtml(
` <span class="ant-tag" style="margin: 0 3px;" contentEditable="false" data-key="${key}">${val}</span> `,
);
const text = getValue(inputRef.current);
onChange(text);
console.log('onChange', text);
}}
>
{numColumns.get(key)}
</button>
</Menu.Item>
))
) : (
<Menu.Item disabled key={0}>
{t('No available fields')}
</Menu.Item>
)}
</Menu>
);
useFormEffects(() => {
onFormSubmitValidateStart(() => {
try {
evaluate(field.value, scope);
field.feedbacks = [];
} catch (e) {
console.error(field.value, scope, (e as Error).message);
field.setFeedback({
type: 'error',
code: 'FormulaError',
messages: [t('Formula error.')],
});
}
});
});
console.log('value, html', value, html);
return (
<Dropdown
overlay={menu}
overlayClassName={css`
.ant-dropdown-menu-item {
padding: 0;
}
button {
cursor: pointer;
padding: 5px 12px;
text-align: left;
color: rgba(0, 0, 0, 0.85);
width: 100%;
line-height: inherit;
height: auto;
border: 0px;
background-color: transparent;
&:hover {
background-color: #f5f5f5;
}
}
`}
>
<div
onKeyDown={(e) => {
const text = getValue(e.currentTarget);
if (e.key === 'Backspace') {
if (text && keys.map((k) => `{{${k}}}`).includes(text)) {
inputRef.current.innerHTML = ' ';
}
}
}}
onKeyUp={(e) => {
const text = getValue(e.currentTarget);
if (e.key === 'Backspace') {
// pasteHtml(' ');
}
console.log('onChange', text);
onChange(text);
}}
onClick={(e) => {
const text = getValue(e.currentTarget);
onChange(text);
console.log('onChange', text);
}}
onBlur={(e) => {
const text = getValue(e.currentTarget);
console.log('onChange', text);
onChange(text);
}}
onInput={(e) => {
const text = getValue(e.currentTarget);
console.log('onChange', text);
onChange(text);
}}
className={'ant-input'}
style={{ display: 'block' }}
ref={inputRef as any}
contentEditable
dangerouslySetInnerHTML={{ __html: html }}
/>
</Dropdown>
);
};
export default Expression;

View File

@ -1,53 +0,0 @@
import { onFormValuesChange } from '@formily/core';
import { useField, useFieldSchema, useForm, useFormEffects } from '@formily/react';
import { toFixedByStep } from '@nocobase/utils/client';
import cloneDeep from 'lodash/cloneDeep';
import * as math from 'mathjs';
import { isNumber } from 'mathjs';
import React, { useState } from 'react';
import { useCollection } from '../collection-manager';
const ReadPretty = (props) => {
if (props?.options?.dataType !== 'string') {
return <div>{toFixedByStep(props.value, props.step)}</div>;
}
return <div>{props.value}</div>;
};
const Input = (props) => {
const { evaluate, options } = props;
const { dataType, expression } = options;
const form = useForm();
const val = () => {
const scope = cloneDeep(form.values);
try {
let result = evaluate(expression, scope);
result = isNumber(result) && Number.isFinite(result) ? math.round(result, 9) : result;
return result;
} catch (error) {
return null;
}
};
const [value, setVal] = useState(() => {
return val();
});
useFormEffects(() => {
onFormValuesChange(() => {
setVal(val());
});
});
if (dataType !== 'string') {
return <div>{toFixedByStep(value, props.step)}</div>;
}
return <div>{value}</div>;
};
export const Result = (props: any) => {
const field = useField();
const { getField } = useCollection();
const fieldSchema = useFieldSchema();
const options = getField(fieldSchema.name as string);
return field.readPretty ? <ReadPretty {...props} options={options} /> : <Input {...props} options={options} />;
};
export default Result;

View File

@ -1,51 +0,0 @@
/**
* title: Formula
*/
import { connect } from '@formily/react';
import { Formula, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const Expression = connect(Formula.Expression);
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
default: '{{f1 }} + {{ f2}}',
'x-component': 'Expression',
'x-component-props': {
evaluate: (exp, scope) => {
return 1;
},
supports: ['number'],
useCurrentFields: () => {
return [
{
name: 'f1',
interface: 'number',
uiSchema: {
title: 'F1',
},
},
{
name: 'f2',
interface: 'number',
uiSchema: {
title: 'F2',
},
},
];
},
},
},
},
};
export default () => {
return (
<SchemaComponentProvider components={{ Expression }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};

View File

@ -1,10 +0,0 @@
---
nav:
path: /client
group:
path: /client
---
# Formula
<code src="./demos/demo2.tsx"/>

View File

@ -1,9 +0,0 @@
import Expression from './Expression';
import Result from './Result';
export const Formula = () => null;
Formula.Result = Result;
Formula.Expression = Expression;
export default Formula;

View File

@ -11,7 +11,6 @@ export * from './board';
export * from './china-region';
export * from './collection-manager';
export * from './document-title';
export * from './formula';
export * from './i18n';
export * from './icon';
export * from './plugin-manager';

View File

@ -0,0 +1,36 @@
import { onFormValuesChange } from '@formily/core';
import { connect, mapReadPretty, useFieldSchema, useFormEffects } from '@formily/react';
import { InputNumber } from 'antd';
import _ from 'lodash';
import * as math from 'mathjs';
import React from 'react';
import { useCollection } from '../../../collection-manager/hooks';
import { ReadPretty } from '../input-number/ReadPretty';
const AntdCompute = (props) => {
const { onChange, ...others } = props;
const { getField } = useCollection();
const fieldSchema = useFieldSchema();
const options = getField(fieldSchema.name);
const { expression } = options;
useFormEffects(() => {
onFormValuesChange((form) => {
const scope = _.cloneDeep(form.values);
let result;
try {
result = math.evaluate(expression, scope);
result = Number.isFinite(result) ? math.round(result, 9) : null;
} catch{}
if (onChange) {
onChange(result);
}
});
});
return <InputNumber {...others} readOnly stringMode={true} />;
};
export const Compute = connect(AntdCompute, mapReadPretty(ReadPretty));
export default Compute;

View File

@ -0,0 +1,121 @@
import { Field, onFormSubmitValidateStart } from '@formily/core';
import { connect, mapProps, useField, useFormEffects } from '@formily/react';
import { Menu, Dropdown } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import ContentEditable from 'react-contenteditable';
import { useTranslation } from 'react-i18next';
import * as math from 'mathjs';
const AntdFormulaInput = (props) => {
const { value, onChange, supports, useCurrentFields } = props;
const field = useField<Field>();
const { t } = useTranslation();
const fields = useCurrentFields();
const inputRef = useRef();
const [dropdownVisible, setDropdownVisible] = useState(false);
const [html, setHtml] = useState(null);
const numColumns = new Map<string, string>();
const scope = {};
fields
.filter((field) => supports.includes(field.interface))
.forEach((field) => {
numColumns.set(field.name, field.uiSchema.title);
scope[field.name] = 1;
});
const keys = Array.from(numColumns.keys());
useEffect(() => {
if (value) {
let newHtml = value;
numColumns.forEach((value, key) => {
newHtml = newHtml.replaceAll(
key,
`<span contentEditable="false" ><input disabled="disabled" style="width:${
18 * value.length
}px;max-width: 120px" value="${value}"/><span hidden>${key}</span></span>`,
);
});
newHtml = `${newHtml}<span style="padding-left: 5px"></span>`; // set extra span for cursor focus on last position
setHtml(newHtml);
} else {
setHtml('');
}
}, [value]);
const menu = (
<Menu
onClick={async (args) => {
const replaceFormula = field.value.replace('@', args.key);
if (onChange && replaceFormula != field.value) {
onChange(replaceFormula);
}
setDropdownVisible(false);
(inputRef.current as any).focus();
}}
>
{keys.map((key) => (
<Menu.Item key={key}>{numColumns.get(key)}</Menu.Item>
))}
</Menu>
);
const handleChange = (e) => {
if (onChange) {
if (e.currentTarget.textContent == '') {
onChange(null);
} else {
onChange(e.currentTarget.textContent);
}
}
};
const handleKeyDown = (e) => {
const { key } = e;
switch (key) {
case 'Enter':
e.preventDefault();
break;
case '@':
case 'Process':
setDropdownVisible(true);
break;
default:
setDropdownVisible(false);
break;
}
};
useFormEffects(() => {
onFormSubmitValidateStart(() => {
try {
math.evaluate(field.value, scope);
field.feedbacks = [];
} catch (e) {
console.error(field.value, scope, (e as Error).message);
field.setFeedback({
type: 'error',
code: 'FormulaError',
messages: [t('Formula error.')],
});
}
});
});
return (
<Dropdown overlay={menu} visible={dropdownVisible}>
<ContentEditable
innerRef={inputRef}
className="ant-input"
onChange={handleChange}
onKeyDown={handleKeyDown}
html={html || ''}
/>
</Dropdown>
);
};
export const FormulaInput = connect(AntdFormulaInput, mapProps({}));
export default FormulaInput;

View File

@ -0,0 +1,6 @@
import { Compute } from './Compute';
import { FormulaInput } from './FormulaInput';
export const Formula: any = () => null;
Formula.Expression = FormulaInput;
Formula.Result = Compute;

View File

@ -4,13 +4,14 @@ export * from './calendar';
export * from './card-item';
export * from './cascader';
export * from './checkbox';
export * from './color-select';
export * from './cron';
export * from './color-select';
export * from './date-picker';
export * from './filter';
export * from './form';
export * from './form-item';
export * from './form-v2';
export * from './formula-input';
export * from './g2plot';
export * from './grid';
export * from './icon-picker';

View File

@ -0,0 +1,69 @@
import { mockDatabase } from '..';
import { Database } from '../../database';
import { FormulaField } from '../../fields';
describe('formula field', () => {
let db: Database;
beforeEach(async () => {
db = mockDatabase();
});
afterEach(async () => {
await db.close();
});
it('add formula field with old table, already has data.', async () => {
const Test = db.collection({
name: 'tests',
fields: [
{ type: 'float', name: 'price' },
{ type: 'float', name: 'count' },
],
});
await db.sync();
const test = await Test.model.create<any>({
price: '1.2',
count: '2',
});
const expression = 'price*count';
const field = Test.addField('sum', { type: 'formula', expression });
await field.sync({});
const updatedTest = await Test.model.findByPk(test.id);
const sum = updatedTest.get('sum');
const sumField = Test.getField<FormulaField>('sum');
expect(sum).toEqual(sumField.caculate(expression, updatedTest.toJSON()));
});
it('auto set formula field with create or update data', async () => {
const expression = 'price*count';
const Test = db.collection({
name: 'tests',
fields: [
{ type: 'float', name: 'price' },
{ type: 'float', name: 'count' },
{ name: 'sum', type: 'formula', expression },
],
});
await db.sync();
const test = await Test.model.create<any>({
price: '1.2',
count: '2',
});
const sumField = Test.getField<FormulaField>('sum');
expect(test.get('sum')).toEqual(sumField.caculate(expression, test.toJSON()));
test.set('count', '6');
await test.save();
expect(test.get('sum')).toEqual(sumField.caculate(expression, test.toJSON()));
});
});

View File

@ -5,12 +5,12 @@ import {
ModelIndexesOptions,
QueryInterfaceOptions,
SyncOptions,
Transactionable
Transactionable,
} from 'sequelize';
import { Collection } from '../collection';
import { Database } from '../database';
import { InheritedCollection } from '../inherited-collection';
import { ModelEventTypes } from '../types';
import { InheritedCollection } from '../inherited-collection';
export interface FieldContext {
database: Database;

View File

@ -1,17 +1,16 @@
import { BaseFieldOptions, Field } from '@nocobase/database';
import * as math from 'mathjs';
import { DataTypes } from 'sequelize';
import { evaluate } from '../utils/evaluate';
import { BaseColumnFieldOptions, Field } from './field';
import * as math from 'mathjs';
export class MathFormulaField extends Field {
export class FormulaField extends Field {
get dataType() {
return DataTypes.DOUBLE;
return DataTypes.FLOAT;
}
calculate(expression, scope) {
let result: any = null;
caculate(expression, scope) {
let result = null;
try {
result = evaluate(expression, scope);
result = math.evaluate(expression, scope);
result = math.round(result, 9);
} catch {}
return result;
@ -27,7 +26,7 @@ export class MathFormulaField extends Field {
for (const record of records) {
const scope = record.toJSON();
const result = this.calculate(expression, scope);
const result = this.caculate(expression, scope);
if (result) {
await record.update(
{
@ -43,12 +42,12 @@ export class MathFormulaField extends Field {
}
};
calculateField = async (instance) => {
caculateField = async (instance) => {
const { expression, name } = this.options;
const scope = instance.toJSON();
let result;
try {
result = evaluate(expression, scope);
result = math.evaluate(expression, scope);
result = math.round(result, 9);
} catch {}
if (result === 0 || result) {
@ -68,7 +67,7 @@ export class MathFormulaField extends Field {
for (const record of records) {
const scope = record.toJSON();
const result = this.calculate(expression, scope);
const result = this.caculate(expression, scope);
await record.update(
{
[name]: result,
@ -88,19 +87,20 @@ export class MathFormulaField extends Field {
this.on('afterSync', this.initFieldData);
// TODO: should not depends on fields table (which is defined by other plugin)
this.database.on('fields.afterUpdate', this.updateFieldData);
this.on('beforeSave', this.calculateField);
this.on('beforeSave', this.caculateField);
}
unbind() {
super.unbind();
this.off('beforeSave', this.calculateField);
this.off('beforeSave', this.caculateField);
// TODO: should not depends on fields table
this.database.off('fields.afterUpdate', this.updateFieldData);
this.off('afterSync', this.initFieldData);
}
}
export interface MathFormulaFieldOptions extends BaseFieldOptions {
type: 'mathFormula';
export interface FormulaFieldOptions extends BaseColumnFieldOptions {
type: 'formula';
expression: string;
}

View File

@ -17,7 +17,6 @@ import {
} from './number-field';
import { PasswordFieldOptions } from './password-field';
import { RadioFieldOptions } from './radio-field';
import { SequenceFieldOptions } from './sequence-field';
import { SortFieldOptions } from './sort-field';
import { StringFieldOptions } from './string-field';
import { TextFieldOptions } from './text-field';
@ -25,6 +24,8 @@ import { TimeFieldOptions } from './time-field';
import { UidFieldOptions } from './uid-field';
import { UUIDFieldOptions } from './uuid-field';
import { VirtualFieldOptions } from './virtual-field';
import { FormulaFieldOptions } from './formula-field';
import { SequenceFieldOptions } from './sequence-field';
export * from './array-field';
export * from './belongs-to-field';
@ -40,7 +41,6 @@ export * from './number-field';
export * from './password-field';
export * from './radio-field';
export * from './relation-field';
export { SequenceField } from './sequence-field';
export * from './sort-field';
export * from './string-field';
export * from './text-field';
@ -48,6 +48,8 @@ export * from './time-field';
export * from './uid-field';
export * from './uuid-field';
export * from './virtual-field';
export * from './formula-field';
export { SequenceField } from './sequence-field';
export type FieldOptions =
| BaseFieldOptions
@ -64,6 +66,7 @@ export type FieldOptions =
| SortFieldOptions
| TextFieldOptions
| VirtualFieldOptions
| FormulaFieldOptions
| ArrayFieldOptions
| TimeFieldOptions
| DateFieldOptions

View File

@ -1,4 +0,0 @@
// @ts-nocheck
export * from './lib/client';
export { default } from './lib/client';

View File

@ -1,30 +0,0 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/client"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -1,12 +0,0 @@
{
"name": "@nocobase/plugin-excel-formula-field",
"version": "0.8.0-alpha.13",
"main": "lib/server/index.js",
"dependencies": {
"@formulajs/formulajs": "^3.1.7"
},
"devDependencies": {
"@nocobase/server": "0.8.0-alpha.13",
"@nocobase/test": "0.8.0-alpha.13"
}
}

View File

@ -1,4 +0,0 @@
// @ts-nocheck
export * from './lib/server';
export { default } from './lib/server';

View File

@ -1,30 +0,0 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/server"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -1,20 +0,0 @@
import { connect, observer } from '@formily/react';
import { Formula } from '@nocobase/client';
import React from 'react';
import { evaluate } from '../../utils/evaluate';
export const ExcelFormula: any = () => null;
ExcelFormula.Result = observer((props) =>
React.createElement(Formula.Result, {
...props,
evaluate,
}),
);
ExcelFormula.Expression = connect((props) => {
return React.createElement(Formula.Expression, {
...props,
evaluate,
});
});

View File

@ -1,56 +0,0 @@
import { IField, interfacesProperties } from '@nocobase/client';
const { defaultProps, operators } = interfacesProperties;
export const excelFormula: IField = {
name: 'excelFormula',
type: 'object',
group: 'advanced',
order: 2,
title: '{{t("Excel formula")}}',
description: '{{t("Compute a value based on the other fields using excel formula functions")}}',
sortable: true,
default: {
type: 'excelFormula',
// name,
uiSchema: {
type: 'string',
// title,
'x-disabled': true,
'x-component': 'ExcelFormula.Result',
'x-component-props': {
stringMode: true,
step: '1',
},
},
},
properties: {
...defaultProps,
dataType: {
type: 'string',
title: '{{t("Data type")}}',
'x-component': 'Select',
'x-decorator': 'FormItem',
default: 'number',
'x-disabled': '{{ !createOnly }}',
enum: [
{ value: 'string', label: '{{t("String")}}' },
{ value: 'number', label: '{{t("Number")}}' },
],
},
expression: {
type: 'string',
title: '{{t("Expression")}}',
required: true,
description: '{{t("Input @ to open field variables.")}}',
'x-component': 'ExcelFormula.Expression',
'x-decorator': 'FormItem',
'x-component-props': {
supports: ['number', 'percent', 'integer', 'string'],
useCurrentFields: '{{ useCurrentFields }}',
},
},
},
filterable: {
operators: operators.string,
},
};

View File

@ -1,17 +0,0 @@
import { CollectionManagerContext, registerField, SchemaComponentOptions } from '@nocobase/client';
import React, { useContext } from 'react';
import { excelFormula } from './excel-formula';
import { ExcelFormula } from './ExcelFormula';
registerField(excelFormula.group, 'excelFormula', excelFormula);
export default React.memo((props) => {
const ctx = useContext(CollectionManagerContext);
return (
<SchemaComponentOptions components={{ ExcelFormula }}>
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, excelFormula } }}>
{props.children}
</CollectionManagerContext.Provider>
</SchemaComponentOptions>
);
});

View File

@ -1 +0,0 @@
export { default } from './server';

View File

@ -1,108 +0,0 @@
import { BaseFieldOptions, Field } from '@nocobase/database';
import { DataTypes } from 'sequelize';
import { evaluate } from '../utils/evaluate';
export class ExcelFormulaField extends Field {
get dataType() {
const { dataType } = this.options;
return dataType === 'string' ? DataTypes.STRING : DataTypes.DOUBLE;
}
calculate(expression, scope) {
try {
return evaluate(expression, scope);
} catch {}
return null;
}
initFieldData = async ({ transaction }) => {
const { expression, name } = this.options;
const records = await this.collection.repository.find({
order: [this.collection.model.primaryKeyAttribute],
transaction,
});
for (const record of records) {
const scope = record.toJSON();
const result = this.calculate(expression, scope);
if (result) {
await record.update(
{
[name]: result,
},
{
transaction,
silent: true,
hooks: false,
},
);
}
}
};
calculateField = async (instance) => {
const { expression, name } = this.options;
const scope = instance.toJSON();
let result = this.calculate(expression, scope);
if (result) {
instance.set(name, result);
}
};
updateFieldData = async (instance, { transaction }) => {
if (this.collection.name === instance.collectionName && instance.name === this.options.name) {
this.options = Object.assign(this.options, instance.options);
const { name, expression } = this.options;
const records = await this.collection.repository.find({
order: [this.collection.model.primaryKeyAttribute],
transaction,
});
for (const record of records) {
const scope = record.toJSON();
const result = this.calculate(expression, scope);
await record.update(
{
[name]: result,
},
{
transaction,
silent: true,
hooks: false,
},
);
}
}
};
bind() {
super.bind();
this.on('afterSync', this.initFieldData);
// TODO: should not depends on fields table (which is defined by other plugin)
this.database.on('fields.afterUpdate', this.updateFieldData);
this.on('beforeSave', this.calculateField);
}
unbind() {
super.unbind();
this.off('beforeSave', this.calculateField);
// TODO: should not depends on fields table
this.database.off('fields.afterUpdate', this.updateFieldData);
this.off('afterSync', this.initFieldData);
}
toSequelize() {
const opts = super.toSequelize();
delete opts.dataType;
return opts;
}
}
export interface ExcelFormulaFieldOptions extends BaseFieldOptions {
type: 'excelFormula';
dataType?: 'number' | 'string';
expression: string;
}

View File

@ -1 +0,0 @@
export { default } from './plugin';

View File

@ -1,24 +0,0 @@
import { InstallOptions, Plugin } from '@nocobase/server';
import { ExcelFormulaField } from './excel-formula-field';
export class ExcelFormulaFieldPlugin extends Plugin {
afterAdd() {}
beforeLoad() {
this.db.registerFieldTypes({
excelFormula: ExcelFormulaField,
});
}
async load() {}
async install(options?: InstallOptions) {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}
export default ExcelFormulaFieldPlugin;

View File

@ -1,9 +0,0 @@
import * as formulajs from '@formulajs/formulajs';
export function evaluate(exp: string, scope = {}) {
const expression = exp.replace(/{{([^}]+)}}/g, (match, i) => {
return scope[i.trim()] || '';
});
const fn = new Function(...Object.keys(formulajs), ...Object.keys(scope), `return ${expression}`);
return fn(...Object.values(formulajs), ...Object.values(scope));
}

View File

@ -1,4 +0,0 @@
// @ts-nocheck
export * from './lib/client';
export { default } from './lib/client';

View File

@ -1,30 +0,0 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/client"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -1,12 +0,0 @@
{
"name": "@nocobase/plugin-math-formula-field",
"version": "0.8.0-alpha.13",
"main": "lib/server/index.js",
"dependencies": {
"mathjs": "^10.6.0"
},
"devDependencies": {
"@nocobase/server": "0.8.0-alpha.13",
"@nocobase/test": "0.8.0-alpha.13"
}
}

View File

@ -1,4 +0,0 @@
// @ts-nocheck
export * from './lib/server';
export { default } from './lib/server';

View File

@ -1,30 +0,0 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/server"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -1,20 +0,0 @@
import { connect, observer } from '@formily/react';
import { Formula } from '@nocobase/client';
import React from 'react';
import { evaluate } from '../../utils/evaluate';
export const MathFormula: any = () => null;
MathFormula.Result = observer((props) =>
React.createElement(Formula.Result, {
...props,
evaluate,
}),
);
MathFormula.Expression = connect((props) => {
return React.createElement(Formula.Expression, {
...props,
evaluate,
});
});

View File

@ -1,17 +0,0 @@
import { CollectionManagerContext, registerField, SchemaComponentOptions } from '@nocobase/client';
import React, { useContext } from 'react';
import { mathFormula } from './math-formula';
import { MathFormula } from './MathFormula';
registerField(mathFormula.group, 'mathFormula', mathFormula);
export default React.memo((props) => {
const ctx = useContext(CollectionManagerContext);
return (
<SchemaComponentOptions components={{ MathFormula }}>
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, mathFormula } }}>
{props.children}
</CollectionManagerContext.Provider>
</SchemaComponentOptions>
);
});

View File

@ -1 +0,0 @@
export { default } from './server';

View File

@ -1,90 +0,0 @@
import { Database, mockDatabase } from '@nocobase/database';
import { MathFormulaField } from '../math-formula-field';
describe('formula field', () => {
let db: Database;
beforeEach(async () => {
db = mockDatabase();
db.registerFieldTypes({
mathFormula: MathFormulaField,
});
});
afterEach(async () => {
await db.close();
});
it('auto set formula field with create or update data', async () => {
const expression = 'price*count';
const Test = db.collection({
name: 'tests',
fields: [
{ type: 'float', name: 'price' },
{ type: 'float', name: 'count' },
{ name: 'sum', type: 'mathFormula', expression },
],
});
await db.sync();
const test = await Test.model.create<any>({
price: '1.2',
count: '2',
});
const sumField = Test.getField('sum');
expect(test.get('sum')).toEqual(2.4);
test.set('count', '6');
await test.save();
expect(test.get('sum')).toEqual(7.2);
});
it('auto set formula field with create or update data', async () => {
const expression = '{{price}}*{{count}}';
const Test = db.collection({
name: 'tests',
fields: [
{ type: 'float', name: 'price' },
{ type: 'float', name: 'count' },
{ name: 'sum', type: 'mathFormula', expression },
],
});
await db.sync();
const test = await Test.model.create<any>({
price: '1.2',
count: '2',
});
const sumField = Test.getField('sum');
expect(test.get('sum')).toEqual(2.4);
test.set('count', '6');
await test.save();
expect(test.get('sum')).toEqual(7.2);
});
it('1.22+2=3.22', async () => {
const expression = '{{a}}+{{b}}';
const Test = db.collection({
name: 'tests',
fields: [
{ type: 'float', name: 'a' },
{ type: 'float', name: 'b' },
{ name: 'sum', type: 'mathFormula', expression },
],
});
await db.sync();
const test = await Test.model.create<any>({
a: '2',
b: '1.22',
});
expect(test.get('sum')).toEqual(3.22);
});
});

View File

@ -1 +0,0 @@
export { default } from './plugin';

View File

@ -1,24 +0,0 @@
import { InstallOptions, Plugin } from '@nocobase/server';
import { MathFormulaField } from './math-formula-field';
export class MathFormulaFieldPlugin extends Plugin {
afterAdd() {}
beforeLoad() {
this.db.registerFieldTypes({
mathFormula: MathFormulaField,
});
}
async load() {}
async install(options?: InstallOptions) {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}
export default MathFormulaFieldPlugin;

View File

@ -1,8 +0,0 @@
import * as math from 'mathjs';
export function evaluate(exp: string, scope = {}) {
const expression = exp.replace(/{{([^}]+)}}/g, (_, i) => {
return scope[i.trim()] || '';
});
return math.evaluate(expression, scope);
}

View File

@ -22,8 +22,6 @@
"@nocobase/plugin-import": "0.8.0-alpha.13",
"@nocobase/plugin-sample-hello": "0.8.0-alpha.13",
"@nocobase/plugin-system-settings": "0.8.0-alpha.13",
"@nocobase/plugin-math-formula-field": "0.8.0-alpha.13",
"@nocobase/plugin-excel-formula-field": "0.8.0-alpha.13",
"@nocobase/plugin-ui-routes-storage": "0.8.0-alpha.13",
"@nocobase/plugin-ui-schema-storage": "0.8.0-alpha.13",
"@nocobase/plugin-users": "0.8.0-alpha.13",

View File

@ -19,8 +19,6 @@ export class PresetNocoBase extends Plugin {
'export',
'import',
'audit-logs',
'math-formula-field',
'excel-formula-field',
];
await this.app.pm.add(plugins, {
enabled: true,