diff --git a/packages/app/client/src/plugins/excel-formula-field.ts b/packages/app/client/src/plugins/excel-formula-field.ts new file mode 100644 index 000000000..e7bcc5fe0 --- /dev/null +++ b/packages/app/client/src/plugins/excel-formula-field.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-excel-formula-field/client'; \ No newline at end of file diff --git a/packages/app/client/src/plugins/math-formula-field.ts b/packages/app/client/src/plugins/math-formula-field.ts new file mode 100644 index 000000000..9869c7c28 --- /dev/null +++ b/packages/app/client/src/plugins/math-formula-field.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-math-formula-field/client'; \ No newline at end of file diff --git a/packages/core/client/src/collection-manager/interfaces/index.ts b/packages/core/client/src/collection-manager/interfaces/index.ts index 383875265..075457a06 100644 --- a/packages/core/client/src/collection-manager/interfaces/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/index.ts @@ -6,7 +6,6 @@ export * from './createdAt'; export * from './createdBy'; export * from './datetime'; export * from './email'; -export * from './formula'; export * from './icon'; export * from './id'; export * from './input'; @@ -31,3 +30,4 @@ export * from './textarea'; export * from './time'; export * from './updatedAt'; export * from './updatedBy'; + diff --git a/packages/core/client/src/formula/Expression.tsx b/packages/core/client/src/formula/Expression.tsx new file mode 100644 index 000000000..c04316a43 --- /dev/null +++ b/packages/core/client/src/formula/Expression.tsx @@ -0,0 +1,220 @@ +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(); + const { t } = useTranslation(); + const fields = useCurrentFields(); + const inputRef = useRef(); + const [changed, setChanged] = useState(false); + + const onChange = (value) => { + setChanged(true); + props.onChange(value); + }; + + const numColumns = new Map(); + 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 + ] = ` ${val} `; + } + return renderExp(value || '', scope); + }); + useEffect(() => { + if (changed) { + return; + } + const scope = {}; + for (const key of keys) { + const val = numColumns.get(key); + scope[ + key + ] = ` ${val} `; + } + const val = renderExp(value || '', scope); + setHtml(val); + }, [value]); + const menu = ( + + {keys.length > 0 ? ( + keys.map((key) => ( + + + + )) + ) : ( + + {t('No available fields')} + + )} + + ); + + 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.')], + }); + } + }); + }); + + return ( + +
{ + 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(' '); + } + onChange(text); + }} + // onClick={(e) => { + // const text = getValue(e.currentTarget); + // onChange(text); + // console.log('onChange', text); + // }} + onBlur={(e) => { + const text = getValue(e.currentTarget); + onChange(text); + }} + onInput={(e) => { + const text = getValue(e.currentTarget); + onChange(text); + }} + className={'ant-input'} + style={{ display: 'block' }} + ref={inputRef as any} + contentEditable + dangerouslySetInnerHTML={{ __html: html }} + /> + + ); +}; + +export default Expression; diff --git a/packages/core/client/src/formula/Result.tsx b/packages/core/client/src/formula/Result.tsx new file mode 100644 index 000000000..e5cd17f1c --- /dev/null +++ b/packages/core/client/src/formula/Result.tsx @@ -0,0 +1,53 @@ +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
{toFixedByStep(props.value, props.step)}
; + } + return
{props.value}
; +}; + +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
{toFixedByStep(value, props.step)}
; + } + return
{value}
; +}; + +export const Result = (props: any) => { + const field = useField(); + const { getField } = useCollection(); + const fieldSchema = useFieldSchema(); + const options = getField(fieldSchema.name as string); + return field.readPretty ? : ; +}; + +export default Result; diff --git a/packages/core/client/src/formula/demos/demo2.tsx b/packages/core/client/src/formula/demos/demo2.tsx new file mode 100644 index 000000000..acab9073e --- /dev/null +++ b/packages/core/client/src/formula/demos/demo2.tsx @@ -0,0 +1,51 @@ +/** + * 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 ( + + + + ); +}; diff --git a/packages/core/client/src/formula/index.md b/packages/core/client/src/formula/index.md new file mode 100644 index 000000000..5efb7dfdf --- /dev/null +++ b/packages/core/client/src/formula/index.md @@ -0,0 +1,10 @@ +--- +nav: + path: /client +group: + path: /client +--- + +# Formula + + diff --git a/packages/core/client/src/formula/index.tsx b/packages/core/client/src/formula/index.tsx new file mode 100644 index 000000000..9a59d66df --- /dev/null +++ b/packages/core/client/src/formula/index.tsx @@ -0,0 +1,9 @@ +import Expression from './Expression'; +import Result from './Result'; + +export const Formula = () => null; + +Formula.Result = Result; +Formula.Expression = Expression; + +export default Formula; diff --git a/packages/core/client/src/index.tsx b/packages/core/client/src/index.tsx index e069e8236..e76fdf3b7 100644 --- a/packages/core/client/src/index.tsx +++ b/packages/core/client/src/index.tsx @@ -11,6 +11,7 @@ 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'; diff --git a/packages/core/client/src/schema-component/antd/formula-input/Compute.tsx b/packages/core/client/src/schema-component/antd/formula-input/Compute.tsx deleted file mode 100644 index e69293d7a..000000000 --- a/packages/core/client/src/schema-component/antd/formula-input/Compute.tsx +++ /dev/null @@ -1,36 +0,0 @@ -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 ; -}; - -export const Compute = connect(AntdCompute, mapReadPretty(ReadPretty)); - -export default Compute; diff --git a/packages/core/client/src/schema-component/antd/formula-input/FormulaInput.tsx b/packages/core/client/src/schema-component/antd/formula-input/FormulaInput.tsx deleted file mode 100644 index 380193f11..000000000 --- a/packages/core/client/src/schema-component/antd/formula-input/FormulaInput.tsx +++ /dev/null @@ -1,120 +0,0 @@ -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(); - const { t } = useTranslation(); - const fields = useCurrentFields(); - - const inputRef = useRef(); - const [dropdownVisible, setDropdownVisible] = useState(false); - const [html, setHtml] = useState(null); - - const numColumns = new Map(); - 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, - ``, - ); - }); - newHtml = `${newHtml}`; // set extra span for cursor focus on last position - setHtml(newHtml); - } else { - setHtml(''); - } - }, [value]); - const items = keys.map((key) => { - return { label: numColumns.get(key), key }; - }); - const menu = ( - { - const replaceFormula = field.value.replace('@', args.key); - if (onChange && replaceFormula != field.value) { - onChange(replaceFormula); - } - setDropdownVisible(false); - (inputRef.current as any).focus(); - }} - items={items} - /> - ); - - 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 ( - - - - ); -}; - -export const FormulaInput = connect(AntdFormulaInput, mapProps({})); - -export default FormulaInput; diff --git a/packages/core/client/src/schema-component/antd/formula-input/index.ts b/packages/core/client/src/schema-component/antd/formula-input/index.ts deleted file mode 100644 index 19f071dcb..000000000 --- a/packages/core/client/src/schema-component/antd/formula-input/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Compute } from './Compute'; -import { FormulaInput } from './FormulaInput'; - -export const Formula: any = () => null; -Formula.Expression = FormulaInput; -Formula.Result = Compute; \ No newline at end of file diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index 12dc544e6..9ed8b810b 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -4,14 +4,13 @@ export * from './calendar'; export * from './card-item'; export * from './cascader'; export * from './checkbox'; -export * from './cron'; export * from './color-select'; +export * from './cron'; 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'; diff --git a/packages/core/database/src/__tests__/fields/formula-field.test.ts b/packages/core/database/src/__tests__/fields/formula-field.test.ts deleted file mode 100644 index 92f561fb2..000000000 --- a/packages/core/database/src/__tests__/fields/formula-field.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -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({ - 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('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({ - price: '1.2', - count: '2', - }); - - const sumField = Test.getField('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())); - }); -}); diff --git a/packages/core/database/src/fields/field.ts b/packages/core/database/src/fields/field.ts index af07e8d14..1bb9d7ad6 100644 --- a/packages/core/database/src/fields/field.ts +++ b/packages/core/database/src/fields/field.ts @@ -6,12 +6,12 @@ import { ModelIndexesOptions, QueryInterfaceOptions, SyncOptions, - Transactionable, + Transactionable } from 'sequelize'; import { Collection } from '../collection'; import { Database } from '../database'; -import { ModelEventTypes } from '../types'; import { InheritedCollection } from '../inherited-collection'; +import { ModelEventTypes } from '../types'; export interface FieldContext { database: Database; diff --git a/packages/core/database/src/fields/index.ts b/packages/core/database/src/fields/index.ts index ccd668fb5..6bce3c26e 100644 --- a/packages/core/database/src/fields/index.ts +++ b/packages/core/database/src/fields/index.ts @@ -13,10 +13,11 @@ import { DoubleFieldOptions, FloatFieldOptions, IntegerFieldOptions, - RealFieldOptions, + RealFieldOptions } from './number-field'; import { PasswordFieldOptions } from './password-field'; import { RadioFieldOptions } from './radio-field'; +import { SetFieldOptions } from './set-field'; import { SortFieldOptions } from './sort-field'; import { StringFieldOptions } from './string-field'; import { TextFieldOptions } from './text-field'; @@ -24,11 +25,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 { SetFieldOptions } from './set-field'; export * from './array-field'; -export * from './set-field'; export * from './belongs-to-field'; export * from './belongs-to-many-field'; export * from './boolean-field'; @@ -42,6 +40,7 @@ export * from './number-field'; export * from './password-field'; export * from './radio-field'; export * from './relation-field'; +export * from './set-field'; export * from './sort-field'; export * from './string-field'; export * from './text-field'; @@ -49,7 +48,6 @@ export * from './time-field'; export * from './uid-field'; export * from './uuid-field'; export * from './virtual-field'; -export * from './formula-field'; export type FieldOptions = | BaseFieldOptions @@ -66,7 +64,6 @@ export type FieldOptions = | SortFieldOptions | TextFieldOptions | VirtualFieldOptions - | FormulaFieldOptions | ArrayFieldOptions | SetFieldOptions | TimeFieldOptions diff --git a/packages/plugins/excel-formula-field/client.d.ts b/packages/plugins/excel-formula-field/client.d.ts new file mode 100755 index 000000000..765db9222 --- /dev/null +++ b/packages/plugins/excel-formula-field/client.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/client'; +export { default } from './lib/client'; + diff --git a/packages/plugins/excel-formula-field/client.js b/packages/plugins/excel-formula-field/client.js new file mode 100755 index 000000000..238820257 --- /dev/null +++ b/packages/plugins/excel-formula-field/client.js @@ -0,0 +1,30 @@ +"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]; + } + }); +}); diff --git a/packages/plugins/excel-formula-field/package.json b/packages/plugins/excel-formula-field/package.json new file mode 100644 index 000000000..0ddd3659f --- /dev/null +++ b/packages/plugins/excel-formula-field/package.json @@ -0,0 +1,12 @@ +{ + "name": "@nocobase/plugin-excel-formula-field", + "version": "0.8.1-alpha.4", + "main": "lib/server/index.js", + "dependencies": { + "@formulajs/formulajs": "^3.1.7" + }, + "devDependencies": { + "@nocobase/server": "0.8.1-alpha.4", + "@nocobase/test": "0.8.1-alpha.4" + } +} diff --git a/packages/plugins/excel-formula-field/server.d.ts b/packages/plugins/excel-formula-field/server.d.ts new file mode 100755 index 000000000..e70edb928 --- /dev/null +++ b/packages/plugins/excel-formula-field/server.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/server'; +export { default } from './lib/server'; + diff --git a/packages/plugins/excel-formula-field/server.js b/packages/plugins/excel-formula-field/server.js new file mode 100755 index 000000000..d06a7eb92 --- /dev/null +++ b/packages/plugins/excel-formula-field/server.js @@ -0,0 +1,30 @@ +"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]; + } + }); +}); diff --git a/packages/plugins/excel-formula-field/src/client/ExcelFormula/index.ts b/packages/plugins/excel-formula-field/src/client/ExcelFormula/index.ts new file mode 100644 index 000000000..b1db4d848 --- /dev/null +++ b/packages/plugins/excel-formula-field/src/client/ExcelFormula/index.ts @@ -0,0 +1,20 @@ +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, + }); +}); diff --git a/packages/plugins/excel-formula-field/src/client/excel-formula.ts b/packages/plugins/excel-formula-field/src/client/excel-formula.ts new file mode 100644 index 000000000..97e3f9184 --- /dev/null +++ b/packages/plugins/excel-formula-field/src/client/excel-formula.ts @@ -0,0 +1,82 @@ +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 }}', + "x-reactions": [ + { + target: 'uiSchema.x-component-props.step', + fulfill: { + state: { + display: '{{$self.value !== "string" ? "visible" : "none"}}', + }, + }, + } + ], + enum: [ + { value: 'string', label: '{{t("String")}}' }, + { value: 'number', label: '{{t("Number")}}' }, + ], + }, + 'uiSchema.x-component-props.step': { + type: 'string', + title: '{{t("Precision")}}', + 'x-component': 'Select', + 'x-decorator': 'FormItem', + required: true, + default: '0', + enum: [ + { value: '0', label: '1' }, + { value: '0.1', label: '1.0' }, + { value: '0.01', label: '1.00' }, + { value: '0.001', label: '1.000' }, + { value: '0.0001', label: '1.0000' }, + { value: '0.00001', label: '1.00000' }, + ], + }, + expression: { + type: 'string', + title: '{{t("Expression")}}', + required: true, + description: '{{excelExpressionDescription}}', + 'x-component': 'ExcelFormula.Expression', + 'x-decorator': 'FormItem', + 'x-component-props': { + supports: ['number', 'percent', 'integer', 'string'], + useCurrentFields: '{{ useCurrentFields }}', + }, + }, + }, + filterable: { + operators: operators.string, + }, +}; diff --git a/packages/plugins/excel-formula-field/src/client/index.tsx b/packages/plugins/excel-formula-field/src/client/index.tsx new file mode 100644 index 000000000..da22017d6 --- /dev/null +++ b/packages/plugins/excel-formula-field/src/client/index.tsx @@ -0,0 +1,34 @@ +import { CollectionManagerContext, registerField, SchemaComponentOptions } from '@nocobase/client'; +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { excelFormula } from './excel-formula'; +import { ExcelFormula } from './ExcelFormula'; + +registerField(excelFormula.group, 'excelFormula', excelFormula); + +export default React.memo((props) => { + const ctx = useContext(CollectionManagerContext); + const { t } = useTranslation(); + + return ( + + {t('Syntax see', { ns: 'math-formula-field' })}{' '} + + formula.js + +
+ ), + }} + components={{ + ExcelFormula, + }} + > + + {props.children} + + + ); +}); diff --git a/packages/plugins/excel-formula-field/src/index.ts b/packages/plugins/excel-formula-field/src/index.ts new file mode 100644 index 000000000..7ddad5814 --- /dev/null +++ b/packages/plugins/excel-formula-field/src/index.ts @@ -0,0 +1 @@ +export { default } from './server'; diff --git a/packages/plugins/excel-formula-field/src/server/excel-formula-field.ts b/packages/plugins/excel-formula-field/src/server/excel-formula-field.ts new file mode 100644 index 000000000..57e8e7502 --- /dev/null +++ b/packages/plugins/excel-formula-field/src/server/excel-formula-field.ts @@ -0,0 +1,108 @@ +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; +} diff --git a/packages/plugins/excel-formula-field/src/server/index.ts b/packages/plugins/excel-formula-field/src/server/index.ts new file mode 100644 index 000000000..b68aea57f --- /dev/null +++ b/packages/plugins/excel-formula-field/src/server/index.ts @@ -0,0 +1 @@ +export { default } from './plugin'; diff --git a/packages/plugins/excel-formula-field/src/server/plugin.ts b/packages/plugins/excel-formula-field/src/server/plugin.ts new file mode 100644 index 000000000..75ed8247c --- /dev/null +++ b/packages/plugins/excel-formula-field/src/server/plugin.ts @@ -0,0 +1,24 @@ +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; diff --git a/packages/plugins/excel-formula-field/src/utils/evaluate.ts b/packages/plugins/excel-formula-field/src/utils/evaluate.ts new file mode 100644 index 000000000..cac69d182 --- /dev/null +++ b/packages/plugins/excel-formula-field/src/utils/evaluate.ts @@ -0,0 +1,9 @@ +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)); +} diff --git a/packages/plugins/math-formula-field/client.d.ts b/packages/plugins/math-formula-field/client.d.ts new file mode 100755 index 000000000..765db9222 --- /dev/null +++ b/packages/plugins/math-formula-field/client.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/client'; +export { default } from './lib/client'; + diff --git a/packages/plugins/math-formula-field/client.js b/packages/plugins/math-formula-field/client.js new file mode 100755 index 000000000..238820257 --- /dev/null +++ b/packages/plugins/math-formula-field/client.js @@ -0,0 +1,30 @@ +"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]; + } + }); +}); diff --git a/packages/plugins/math-formula-field/package.json b/packages/plugins/math-formula-field/package.json new file mode 100644 index 000000000..838402cca --- /dev/null +++ b/packages/plugins/math-formula-field/package.json @@ -0,0 +1,12 @@ +{ + "name": "@nocobase/plugin-math-formula-field", + "version": "0.8.1-alpha.4", + "main": "lib/server/index.js", + "dependencies": { + "mathjs": "^10.6.0" + }, + "devDependencies": { + "@nocobase/server": "0.8.1-alpha.4", + "@nocobase/test": "0.8.1-alpha.4" + } +} diff --git a/packages/plugins/math-formula-field/server.d.ts b/packages/plugins/math-formula-field/server.d.ts new file mode 100755 index 000000000..e70edb928 --- /dev/null +++ b/packages/plugins/math-formula-field/server.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/server'; +export { default } from './lib/server'; + diff --git a/packages/plugins/math-formula-field/server.js b/packages/plugins/math-formula-field/server.js new file mode 100755 index 000000000..d06a7eb92 --- /dev/null +++ b/packages/plugins/math-formula-field/server.js @@ -0,0 +1,30 @@ +"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]; + } + }); +}); diff --git a/packages/plugins/math-formula-field/src/client/MathFormula/index.tsx b/packages/plugins/math-formula-field/src/client/MathFormula/index.tsx new file mode 100644 index 000000000..9d62ae7b9 --- /dev/null +++ b/packages/plugins/math-formula-field/src/client/MathFormula/index.tsx @@ -0,0 +1,20 @@ +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, + }); +}); diff --git a/packages/plugins/math-formula-field/src/client/index.tsx b/packages/plugins/math-formula-field/src/client/index.tsx new file mode 100644 index 000000000..9164c55c5 --- /dev/null +++ b/packages/plugins/math-formula-field/src/client/index.tsx @@ -0,0 +1,31 @@ +import { CollectionManagerContext, registerField, SchemaComponentOptions } from '@nocobase/client'; +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { mathFormula } from './math-formula'; +import { MathFormula } from './MathFormula'; + +registerField(mathFormula.group, 'mathFormula', mathFormula); + +export default React.memo((props) => { + const ctx = useContext(CollectionManagerContext); + const { t } = useTranslation(); + return ( + + {t('Syntax see', { ns: 'math-formula-field' })}{' '} + + math.js + + + ), + }} + components={{ MathFormula }} + > + + {props.children} + + + ); +}); diff --git a/packages/core/client/src/collection-manager/interfaces/formula.ts b/packages/plugins/math-formula-field/src/client/math-formula.tsx similarity index 61% rename from packages/core/client/src/collection-manager/interfaces/formula.ts rename to packages/plugins/math-formula-field/src/client/math-formula.tsx index 99e41a26c..743686989 100644 --- a/packages/core/client/src/collection-manager/interfaces/formula.ts +++ b/packages/plugins/math-formula-field/src/client/math-formula.tsx @@ -1,22 +1,22 @@ -import { defaultProps, operators } from './properties'; -import { IField } from './types'; +import { IField, interfacesProperties } from '@nocobase/client'; +const { defaultProps, operators } = interfacesProperties; -export const formula: IField = { - name: 'formula', +export const mathFormula: IField = { + name: 'mathFormula', type: 'object', group: 'advanced', order: 1, - title: '{{t("Formula")}}', - description: '{{t("Formula description")}}', + title: '{{t("Math formula")}}', + description: '{{t("Compute a value based on the other fields using mathjs")}}', sortable: true, default: { - type: 'formula', + type: 'mathFormula', // name, uiSchema: { type: 'number', // title, - "x-disabled": true, - 'x-component': 'Formula.Result', + 'x-disabled': true, + 'x-component': 'MathFormula.Result', 'x-component-props': { stringMode: true, step: '1', @@ -25,16 +25,16 @@ export const formula: IField = { }, properties: { ...defaultProps, - 'expression': { + expression: { type: 'string', title: '{{t("Expression")}}', required: true, - description: '{{t("Input +, -, *, /, ( ) to calculate, input @ to open field variables.")}}', - 'x-component': 'Formula.Expression', + description: '{{mathExpressionDescription}}', + 'x-component': 'MathFormula.Expression', 'x-decorator': 'FormItem', 'x-component-props': { - 'supports': ['number', 'percent', 'integer'], - 'useCurrentFields': '{{ useCurrentFields }}' + supports: ['number', 'percent', 'integer'], + useCurrentFields: '{{ useCurrentFields }}', }, }, 'uiSchema.x-component-props.step': { diff --git a/packages/plugins/math-formula-field/src/index.ts b/packages/plugins/math-formula-field/src/index.ts new file mode 100644 index 000000000..7ddad5814 --- /dev/null +++ b/packages/plugins/math-formula-field/src/index.ts @@ -0,0 +1 @@ +export { default } from './server'; diff --git a/packages/plugins/math-formula-field/src/server/__tests__/formula-field.test.ts b/packages/plugins/math-formula-field/src/server/__tests__/formula-field.test.ts new file mode 100644 index 000000000..8be8b5302 --- /dev/null +++ b/packages/plugins/math-formula-field/src/server/__tests__/formula-field.test.ts @@ -0,0 +1,90 @@ +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({ + 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({ + 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({ + a: '2', + b: '1.22', + }); + + expect(test.get('sum')).toEqual(3.22); + }); +}); diff --git a/packages/plugins/math-formula-field/src/server/index.ts b/packages/plugins/math-formula-field/src/server/index.ts new file mode 100644 index 000000000..b68aea57f --- /dev/null +++ b/packages/plugins/math-formula-field/src/server/index.ts @@ -0,0 +1 @@ +export { default } from './plugin'; diff --git a/packages/core/database/src/fields/formula-field.ts b/packages/plugins/math-formula-field/src/server/math-formula-field.ts similarity index 76% rename from packages/core/database/src/fields/formula-field.ts rename to packages/plugins/math-formula-field/src/server/math-formula-field.ts index db56bc7f2..1f94936db 100644 --- a/packages/core/database/src/fields/formula-field.ts +++ b/packages/plugins/math-formula-field/src/server/math-formula-field.ts @@ -1,16 +1,17 @@ -import { DataTypes } from 'sequelize'; -import { BaseColumnFieldOptions, Field } from './field'; +import { BaseFieldOptions, Field } from '@nocobase/database'; import * as math from 'mathjs'; +import { DataTypes } from 'sequelize'; +import { evaluate } from '../utils/evaluate'; -export class FormulaField extends Field { +export class MathFormulaField extends Field { get dataType() { - return DataTypes.FLOAT; + return DataTypes.DOUBLE; } - caculate(expression, scope) { - let result = null; + calculate(expression, scope) { + let result: any = null; try { - result = math.evaluate(expression, scope); + result = evaluate(expression, scope); result = math.round(result, 9); } catch {} return result; @@ -26,7 +27,7 @@ export class FormulaField extends Field { for (const record of records) { const scope = record.toJSON(); - const result = this.caculate(expression, scope); + const result = this.calculate(expression, scope); if (result) { await record.update( { @@ -42,12 +43,12 @@ export class FormulaField extends Field { } }; - caculateField = async (instance) => { + calculateField = async (instance) => { const { expression, name } = this.options; const scope = instance.toJSON(); let result; try { - result = math.evaluate(expression, scope); + result = evaluate(expression, scope); result = math.round(result, 9); } catch {} if (result === 0 || result) { @@ -67,7 +68,7 @@ export class FormulaField extends Field { for (const record of records) { const scope = record.toJSON(); - const result = this.caculate(expression, scope); + const result = this.calculate(expression, scope); await record.update( { [name]: result, @@ -87,20 +88,19 @@ export class FormulaField 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.caculateField); + this.on('beforeSave', this.calculateField); } unbind() { super.unbind(); - this.off('beforeSave', this.caculateField); + this.off('beforeSave', this.calculateField); // TODO: should not depends on fields table this.database.off('fields.afterUpdate', this.updateFieldData); this.off('afterSync', this.initFieldData); } } -export interface FormulaFieldOptions extends BaseColumnFieldOptions { - type: 'formula'; - +export interface MathFormulaFieldOptions extends BaseFieldOptions { + type: 'mathFormula'; expression: string; } diff --git a/packages/plugins/math-formula-field/src/server/migrations/20230110103214-alert-field.ts b/packages/plugins/math-formula-field/src/server/migrations/20230110103214-alert-field.ts new file mode 100644 index 000000000..e3db4f804 --- /dev/null +++ b/packages/plugins/math-formula-field/src/server/migrations/20230110103214-alert-field.ts @@ -0,0 +1,24 @@ +import { Migration } from '@nocobase/server'; + +export default class extends Migration { + async up() { + const result = await this.app.version.satisfies('<=0.8.1-alpha.4'); + if (!result) { + return; + } + const Field = this.context.db.getRepository('fields'); + const fields = await Field.find(); + await this.context.db.sequelize.transaction(async (transaction) => { + for (const field of fields) { + if (field.get('type') === 'formula') { + field.set('type', 'mathFormula'); + field.set('interface', 'mathFormula'); + await field.save({ transaction }); + const schema = await field.getUiSchema({ transaction }); + schema.set('x-component', 'MathFormula.Result'); + await schema.save({ transaction }); + } + } + }); + } +} diff --git a/packages/plugins/math-formula-field/src/server/plugin.ts b/packages/plugins/math-formula-field/src/server/plugin.ts new file mode 100644 index 000000000..47ce0fbe7 --- /dev/null +++ b/packages/plugins/math-formula-field/src/server/plugin.ts @@ -0,0 +1,34 @@ +import { InstallOptions, Plugin } from '@nocobase/server'; +import { resolve } from 'path'; +import { MathFormulaField } from './math-formula-field'; + +export class MathFormulaFieldPlugin extends Plugin { + afterAdd() {} + + beforeLoad() { + this.db.registerFieldTypes({ + formula: MathFormulaField, + mathFormula: MathFormulaField, + }); + + this.db.addMigrations({ + namespace: this.name, + directory: resolve(__dirname, './migrations'), + context: { + plugin: this, + }, + }); + } + + async load() {} + + async install(options?: InstallOptions) {} + + async afterEnable() {} + + async afterDisable() {} + + async remove() {} +} + +export default MathFormulaFieldPlugin; diff --git a/packages/plugins/math-formula-field/src/utils/evaluate.ts b/packages/plugins/math-formula-field/src/utils/evaluate.ts new file mode 100644 index 000000000..71c0eee14 --- /dev/null +++ b/packages/plugins/math-formula-field/src/utils/evaluate.ts @@ -0,0 +1,8 @@ +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); +} diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index 4a58ea6e0..16ab0d4c5 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -21,6 +21,8 @@ "@nocobase/plugin-export": "0.8.1-alpha.4", "@nocobase/plugin-file-manager": "0.8.1-alpha.4", "@nocobase/plugin-iframe-block": "0.8.1-alpha.4", + "@nocobase/plugin-math-formula-field": "0.8.1-alpha.4", + "@nocobase/plugin-excel-formula-field": "0.8.1-alpha.4", "@nocobase/plugin-import": "0.8.1-alpha.4", "@nocobase/plugin-map": "0.8.1-alpha.4", "@nocobase/plugin-oidc": "0.8.1-alpha.4", diff --git a/packages/presets/nocobase/src/index.ts b/packages/presets/nocobase/src/index.ts index 64f55f2cd..b42dee806 100644 --- a/packages/presets/nocobase/src/index.ts +++ b/packages/presets/nocobase/src/index.ts @@ -25,6 +25,8 @@ export class PresetNocoBase extends Plugin { 'audit-logs', 'duplicator', 'iframe-block', + 'math-formula-field', + 'excel-formula-field', ].concat(plugins), ); } diff --git a/packages/pro-plugins b/packages/pro-plugins index 016bec621..5a5fba41a 160000 --- a/packages/pro-plugins +++ b/packages/pro-plugins @@ -1 +1 @@ -Subproject commit 016bec621e2576f4cda176cf3d42c9eb7e2eb857 +Subproject commit 5a5fba41ae1cecaf3b073fcb610e9dc68dad0b85 diff --git a/yarn.lock b/yarn.lock index d9571a30f..3ae5e5a68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -355,7 +355,7 @@ resolved "https://registry.npmjs.org/@antv/x6-react-shape/-/x6-react-shape-1.6.3.tgz#053660bd555c4356e9719134e3e2a857b51301e2" integrity sha512-iCsRVkvKCVs7HUUblF/ALg5XxeeEPXDLHuYzRvbvNlCKKLbXLbaz6vJQSgTTrk2i4lrD+7xl4riA76FyWA3k/g== -"@antv/x6@^1.9.0": +"@antv/x6@^1.34.2": version "1.34.6" resolved "https://registry.npmjs.org/@antv/x6/-/x6-1.34.6.tgz#b802b09b52c8620acf01734397714ea5e3f715b6" integrity sha512-cl8ywN7CTNVT94/ZEbq2mnZQTQe6FUgIlGOAjNOPL8sljZTBrpE6OsuGw4BDA2/XllwELfUfvVpgBvbdwRKrxw== @@ -3274,6 +3274,14 @@ dependencies: "@formily/shared" "2.0.20" +"@formulajs/formulajs@^3.1.7": + version "3.2.0" + resolved "https://registry.npmjs.org/@formulajs/formulajs/-/formulajs-3.2.0.tgz#78425f4d49cee49d7d2169c2b9d0bccf2f92afa6" + integrity sha512-6rkarI3MTpOTWlvjG1AqRvF/M15Dga/Y6qDGOpxI5ekJGDZKpKRiItTAZ9VEZiDKvlZc42qkRy6tIV5FwHPuAw== + dependencies: + bessel "^1.0.2" + jstat "^1.9.6" + "@gar/promisify@^1.0.1": version "1.1.2" resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" @@ -4411,6 +4419,153 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@nocobase/acl@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/acl/-/acl-0.8.0-alpha.9.tgz#7ffab4a367befc74daa681b1d5c2c907126cb280" + integrity sha512-jEgpjO6gGi4GwSyWxlmcwd368/AQgmrdsODDTLhhrJJI9+ovj2mxO664lQgh3FkDGtmduKlBiZ+qcgK+/RWa1w== + dependencies: + "@nocobase/resourcer" "0.8.0-alpha.9" + json-templates "^4.2.0" + +"@nocobase/actions@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/actions/-/actions-0.8.0-alpha.9.tgz#3804bbf30f7c4e8aa205b9802ec4f56a58c18c72" + integrity sha512-EvdNjXa+TTMU2Xn+MnA7tSa5rooGFtzsvJsSYbkpODtsVQsmTDRIZYZsOYFk/XcezxjzTTsxxXMSA7sLq6wifQ== + dependencies: + "@nocobase/cache" "0.8.0-alpha.9" + "@nocobase/database" "0.8.0-alpha.9" + "@nocobase/resourcer" "0.8.0-alpha.9" + +"@nocobase/cache@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/cache/-/cache-0.8.0-alpha.9.tgz#99f7ca196dacd41b69ed7b7f179fcc1c4c5baa27" + integrity sha512-GCi7OE9nLrXKq/Og6SPtPcjtreUxxAL9x7CwmD2x3Zb7gNTc2GvQ9NWoAlO8KTLlWuzuNcJXegBhxaxV8Lt5ew== + dependencies: + cache-manager "^4.1.0" + +"@nocobase/client@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/client/-/client-0.8.0-alpha.9.tgz#03446b4c4160a7ebda9dfd8aa80e2203541eb72c" + integrity sha512-cvh01k8Ujhc9D7kDYQ7QKP5tnLF+wBOqixtisivti9aeklMBaOKj1oLmxKHZlB2IJzKpT+rUntpsxGxUS6co9A== + dependencies: + "@antv/g2plot" "^2.4.18" + "@dnd-kit/core" "^5.0.1" + "@dnd-kit/sortable" "^6.0.0" + "@emotion/css" "^11.7.1" + "@formily/antd" "2.0.20" + "@formily/core" "2.0.20" + "@formily/react" "2.0.20" + "@nocobase/sdk" "0.8.0-alpha.9" + "@nocobase/utils" "0.8.0-alpha.9" + ahooks "^3.0.5" + antd "~4.19.5" + axios "^0.26.1" + classnames "^2.3.1" + cron-parser "^4.6.0" + cronstrue "^2.11.0" + file-saver "^2.0.5" + i18next "^21.6.0" + json-templates "^4.2.0" + marked "^4.0.12" + mathjs "^10.6.0" + react-beautiful-dnd "^13.1.0" + react-big-calendar "^0.38.7" + react-contenteditable "^3.3.6" + react-drag-listview "^0.1.9" + react-helmet "^6.1.0" + react-hotkeys-hook "^3.4.7" + react-i18next "^11.15.1" + react-image-lightbox "^5.1.4" + react-js-cron "^1.4.0" + react-quill "^1.3.5" + react-router-dom "^5.2.0" + react-to-print "^2.14.7" + solarlunar-es "^1.0.9" + use-deep-compare-effect "^1.8.1" + +"@nocobase/database@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/database/-/database-0.8.0-alpha.9.tgz#9684b4ddcfc5396df2ef3d4ae9cd2ce59142df81" + integrity sha512-zkLTW2iFg1tlbSTlW40oEmM7O2GxHXmIzErnDR4it0GWoLqjDgzNHP5QjODOxiiB/+/unNFudfGTNCoXxu2ZvA== + dependencies: + "@nocobase/utils" "0.8.0-alpha.9" + async-mutex "^0.3.2" + cron-parser "4.4.0" + deepmerge "^4.2.2" + flat "^5.0.2" + glob "^7.1.6" + mathjs "^10.6.1" + moment "2.x" + semver "^7.3.7" + sequelize "^6.9.0" + umzug "^3.1.1" + +"@nocobase/resourcer@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/resourcer/-/resourcer-0.8.0-alpha.9.tgz#5415af5944b31293e5e2da73bf5c999866a90fab" + integrity sha512-6v9bfUex2RfT8C3xX+SUhj8/zECCY6pQA3ce8GV3xd9cu6ku8c7hIiNM3KwgPOD189aND3FxNwxqpECNaoWQ1A== + dependencies: + "@nocobase/utils" "0.8.0-alpha.9" + deepmerge "^4.2.2" + koa-compose "^4.1.0" + lodash "^4.17.21" + path-to-regexp "^6.1.0" + qs "^6.9.4" + +"@nocobase/sdk@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/sdk/-/sdk-0.8.0-alpha.9.tgz#5306e7bf4aabf00894a65036c584508d93bda379" + integrity sha512-v+GF4N5ldYwmoqQIukGWzz/eOm2TgxcUZq3uE+YeP/aiDa4XAfkLrpBNsvCS24k3s/U+84FYrvTWRRStJGSzBA== + dependencies: + axios "^0.26.1" + qs "^6.10.1" + +"@nocobase/server@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/server/-/server-0.8.0-alpha.9.tgz#bc1955e41b2ac9f6640fdf109516376604d4092c" + integrity sha512-/mHk2Uc8RS/3GcGvnMnFKlbf2aCufGuc+GOr5gEKuyWa50ibD4nZ7LKJx023uEO07rxWkFkhR6+ExuZ+BGRDGQ== + dependencies: + "@hapi/topo" "^6.0.0" + "@koa/cors" "^3.1.0" + "@koa/router" "^9.4.0" + "@nocobase/acl" "0.8.0-alpha.9" + "@nocobase/actions" "0.8.0-alpha.9" + "@nocobase/database" "0.8.0-alpha.9" + "@nocobase/resourcer" "0.8.0-alpha.9" + chalk "^4.1.1" + commander "^9.2.0" + find-package-json "^1.2.0" + i18next "^21.6.0" + koa "^2.13.4" + koa-bodyparser "^4.3.0" + koa-static "^5.0.0" + lodash "^4.17.21" + semver "^7.3.7" + xpipe "^1.0.5" + +"@nocobase/test@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/test/-/test-0.8.0-alpha.9.tgz#40aaed50af9b8fd23c0666156c806273c75b0a3f" + integrity sha512-bpzcogPKzfq8mjIsLDy0xlsd0dNAUgmju9kk+RPV5wNZfhvofaA7YduexawbL+dLJQC+iyjmhAhARxl8rVhKuA== + dependencies: + "@nocobase/server" "0.8.0-alpha.9" + "@types/supertest" "^2.0.11" + mockjs "^1.1.0" + mysql2 "^2.3.3" + pg "^8.7.3" + pg-hstore "^2.3.4" + sqlite3 "^5.0.8" + supertest "^6.1.6" + +"@nocobase/utils@0.8.0-alpha.9": + version "0.8.0-alpha.9" + resolved "https://registry.npmjs.org/@nocobase/utils/-/utils-0.8.0-alpha.9.tgz#fa7e203412e91ab7a6ebb12a6d53d83e09465591" + integrity sha512-oEqKs4+0sO4x0TFEtf+Tf1nIkb9q+9y9YmrqOn6MyMo/ejTSh6nlNt1rAYgs02gTP4uHkSOb2/4K4oDm0JXLdg== + dependencies: + "@hapi/topo" "^6.0.0" + deepmerge "^4.2.2" + flat-to-nested "^1.1.1" + "@node-saml/node-saml@^4.0.2": version "4.0.2" resolved "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.2.tgz#e12020ea635346f33fcef008fe0e6fa2f28713e5" @@ -6234,6 +6389,21 @@ ahooks-v3-count@^1.0.0: resolved "https://registry.yarnpkg.com/ahooks-v3-count/-/ahooks-v3-count-1.0.0.tgz#ddeb392e009ad6e748905b3cbf63a9fd8262ca80" integrity sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ== +ahooks@^3.0.5: + version "3.7.4" + resolved "https://registry.npmjs.org/ahooks/-/ahooks-3.7.4.tgz#25e76a989da324a855eb75758b789ab0644b7a59" + integrity sha512-hvgdqzPUKXn95mK3cGlDCi/ZZqv+FRibCUCFT8zW3hCwLGvixVfnHrIW2/2lgzPdLo8mLjp/XOdIJvcPvE2lgQ== + dependencies: + "@types/js-cookie" "^2.x.x" + ahooks-v3-count "^1.0.0" + dayjs "^1.9.1" + intersection-observer "^0.12.0" + js-cookie "^2.x.x" + lodash "^4.17.21" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + tslib "^2.4.1" + ahooks@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/ahooks/-/ahooks-3.7.2.tgz#0afa42625e77ae1cc4b60b19c45cf12a8cf29b56" @@ -7380,6 +7550,11 @@ before-after-hook@^2.2.0: resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== +bessel@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/bessel/-/bessel-1.0.2.tgz#828812291e0b62e94959cdea43fac186e8a7202d" + integrity sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -14747,6 +14922,11 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jstat@^1.9.6: + version "1.9.6" + resolved "https://registry.npmjs.org/jstat/-/jstat-1.9.6.tgz#60e801b0d4c26e37aab0f375d1859fe9d60e10c0" + integrity sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug== + jstoxml@^2.0.0: version "2.2.9" resolved "https://registry.yarnpkg.com/jstoxml/-/jstoxml-2.2.9.tgz#2eebd5e55383fe66a375022ca0aa88f77bc4fb84" @@ -21020,6 +21200,11 @@ retry-as-promised@^6.1.0: resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-6.1.0.tgz#11eca9a0f97804d552ec8e74bc4eb839bd226dc4" integrity sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA== +retry-as-promised@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.3.tgz#ca3c13b15525a7bfbf0f56d2996f0e75649d068b" + integrity sha512-SEvMa4khHvpU/o6zgh7sK24qm6rxVgKnrSyzb5POeDvZx5N9Bf0s5sQsQ4Fl+HjRp0X+w2UzACGfUnXtx6cJ9Q== + retry@^0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -21452,6 +21637,28 @@ sequelize@^6.26.0: validator "^13.7.0" wkx "^0.5.0" +sequelize@^6.9.0: + version "6.28.0" + resolved "https://registry.npmjs.org/sequelize/-/sequelize-6.28.0.tgz#d6bc4e36647e8501635467c0777c45a33f5d5ba8" + integrity sha512-+WHqvUQgTp19GLkt+gyQ+F6qg+FIEO2O5F9C0TOYV/PjZ2a/XwWvVkL1NCkS4VSIjVVvAUutiW6Wv9ofveGaVw== + dependencies: + "@types/debug" "^4.1.7" + "@types/validator" "^13.7.1" + debug "^4.3.3" + dottie "^2.0.2" + inflection "^1.13.2" + lodash "^4.17.21" + moment "^2.29.1" + moment-timezone "^0.5.34" + pg-connection-string "^2.5.0" + retry-as-promised "^7.0.3" + semver "^7.3.5" + sequelize-pool "^7.1.0" + toposort-class "^1.0.1" + uuid "^8.3.2" + validator "^13.7.0" + wkx "^0.5.0" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -23072,6 +23279,11 @@ tslib@^2.1.0, tslib@^2.2.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + tsscmp@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"