refactor: formula plugin (#1082)
* fix: transaction cannot be rolled back because it has been finished with state: rollback * fix(plugin-system-settings): convert array to json * fix(collection-manager): o2m is array type * fix: missing RefreshActionInitializer * fix(collection-manger): incorrect scope key parameter * fix: can't access pages without permission via url (#826) * feat(database): add sequence field type (#779) * feat(database): add serialString field type * feat(database): add serial string type field ui (skip ci) * test(feat/database): test field options * docs: demo * fix(database): fix array table field behavior * fix(database): fix serial type interface ui * fix(database): add match logic for patterns changes * fix(database): fix serial type query last bug in mysql * refactor(database): refactor last record logic * chore: revert modification on unnecessary file * refactor(database): rename serialString type to sequence Co-authored-by: chenos <chenlinxh@gmail.com> * added Russian translation (#840) * Russian translation * Add files via upload Add RU locale into index.ts Bugs fixed in the ru_RU.ts * Update index.ts Correct lines 4 and 8 * feat: update option must have filter or filterByTk (#847) * feat: update option must have filter or filterByTk * fix: typo * fix: typo * feat(core/cache): support cache (#876) * feat(core/cache): support cache * build(create-nocobase-app): remove --cache-store-package cli option * perf(core/cache): modify default cache config and remove unnecessary logic code * fix: slow join query issued by appends field in find method of repository (#845) * fix: slow join query issue by appends field in repository.find * feat: handle appending query in multiple relation repository * feat: handle appending query in single relation repository Co-authored-by: chenos <chenlinxh@gmail.com> * fix: sort parameter is missing (#849) * fix: 审计日志翻页sort丢失 * fix: 审计日志翻页sort丢失 * fix: 审计日志翻页sort丢失 Co-authored-by: 唐小爱 <tangxiaoai@192.168.0.103> * fix(formula): support integer and fix NaN error (#879) * fix(formula): support integer and fix NaN error * style(formula-input): remove debugger * fix(database): fix the index name too long error * feat(collection-manager): inverse fields can be configured (#883) * feat: inverse field * feat: improve code * feat: translations * fix: required * fix: run test by jest (#891) * fix: unable to submit form during file upload (#892) * fix(client/block-select-collection): fix select collection menu view error (#889) * fix(client/block-select-collection): fix too many collection menu view error * fix(client/relate-collection-field-menu): fix relate collection field menu view too long error * fix(client/record-picker): support record-picker show format DataPicker (#888) * fix(client/record-picker): support record-picker show format DataPicker * fix(client/record-picker): undefined judgment and when change field's label refresh format in time * feat: improve signin and signup page components * feat(plugin-workflow): add concat calculator (#894) * fix: single relation repository appends query issue (#901) * fix: appends merge includes (#905) * fix: build error * fix(client): tab pane initializers for create form block * fix: version judgment is not accurate * fix: sync collection field default value (#907) * feat: limit database identifier (#908) * fix: cannot read properties of undefined (reading 'target') * fix: appends merge now using primary key (#911) * fix: appends merge now using primary key * chore: console.log * fix: unbind on error throwing (#914) * feat: create with array of values (#912) * feat: create with array of values * chore: console.log * chore: debug * fix(client/route-switch): skip sub routes * Feat: plugin workflow collection field (#919) * feat(plugin-workflow): use Collectionfield component to render form * fix(plugin-workflow): fix association types value assigning in nodes * fix: missing menuItemGroupCss * fix: multiple = false * chore(versions): 😊 publish v0.7.5-alpha.1 (#920) * fix(plugin-workflow): temp disable validation of collection field in node (#928) * fix(plugin-workflow): fix schedule infinitely trigger when repeat not set (#926) * Feat/plugin workflow collection field (#934) * feat(plugin-workflow): support association constant simple input * fix(plugin-workflow): remove useless code * fix(plugin-workflow): add req context to processor (#936) * fix(plugin-workflow): avoid revision with ghost nodes (#941) * fix(plugin-workflow): fix collection fieldset component (#942) * feat(useSignup): customize success message * feat: changed with associations (#943) * feat: changed with associations * fix: test error * fix: test error * fix: test * fix(plugin-workflow): fix schedule trigger bug (#949) * fix(plugin-workflow): skip time based test (#951) * fix(plugin-workflow): test changedWithAssociations() (#950) * fix(plugin-workflow): test changedWithAssociations() * fix: toChangedWithAssociations * fix(plugin-workflow): add test cases Co-authored-by: chenos <chenlinxh@gmail.com> * feat: docker optimizing (#948) * chore: feat/docker-optimizing * feat: docker nginx gzip config * feat: ui schema cache (#877) * feat(core/cache): support cache * perf(plugins/ui-schema-storage): cache schema * refactor(plugins/ui-schema-storage): handle schema cache in repository level, not in action * fix(plugins/ui-schema-storage): jsonSchema use s_ + x-uid and properties use p_ + x-uid cache prevent jsonSchema and properties cache override each other * test(plugins/ui-schema-storage): add ui_schema repository with cache test * build(create-nocobase-app): remove create-nocobase cli's cache-store-package option * test(plugins/ui-schema-storage): add ui_schema repository with cache test with readFromCache false * fix(plugins/ui-schema-storage): repository insertAdjacent and patch method clear cache fix Co-authored-by: chenos <chenlinxh@gmail.com> * feat: add filter action to collection table (#953) * feat: 数据表支持筛选 * feat: 数据表支持筛选 * feat: 数据表支持筛选 * feat: 数据表支持筛选 * fix: format code Co-authored-by: chenos <chenlinxh@gmail.com> * fix: default value of VERDACCIO_URL * feat: add Dockerfile.official * refactor(plugin-file-manager): move client code into plugin folder and enable path config (#913) * Turkish language (#939) * Türkçe dil dosyası eklendi. Turkish language added * Türkçe dil düzenlemesi. Turkish language edit. * fix: cannot find name 'antdTrTR' * chore(versions): 😊 publish v0.7.6-alpha.2 (#954) * Turkish readme (#955) * Türkçe dil dosyası eklendi. Turkish language added * Türkçe Açıklama sayfası. Turkish readme file added * Turkish language link added * turkish language link added * fix: update readme * fix(plugin-workflow): fix constant schedule trigger time (#956) * feat: push ali docker registry (#957) * feat: push ali docker registry * fix: add VERDACCIO_URL * fix: env * fix: add VERDACCIO_URL to env * fix: update VERDACCIO_URL * fix: arg VERDACCIO_URL * fix: update Dockerfile.official * fix: build-args * fix: network: host * fix: ci * fix: verdaccio:10104 * Update branch-docker-ci.yml * Update branch-docker-ci.yml * Update branch-docker-ci.yml * Update branch-docker-ci.yml * Update branch-docker-ci.yml * Update branch-docker-ci.yml * Update branch-docker-ci.yml * feat: push acr Co-authored-by: ChengLei Shao <chareice@live.com> * feat: api service * fix: api services * feat(file-manager): support tencent cos (#958) * feat: support tencent cos * feat: normalize * chore: update env example * test: add cos * Revert "feat: normalize" This reverts commit 371880013c20b4cb4174ffaa15e0157acc5306c3. * feat: normalize * feat: tengxun to tencent * chore: fix missing deps * fix: locale * fix: locale cos * fix(database/formula-field): when formula's field caculate result is 0 it alse will be save (#962) * fix: lerna ERR! EUNCOMMIT M yarn.lock * fix: driver-opts: network=host * fix(ci): add verdaccio service * chore(versions): 😊 publish v0.7.7-alpha.1 (#963) * fix(plugin-workflow): fix workflow update action (#964) * fix(ci): yarn config set network-timeout 600000 * fix: empty logic operator filter (#961) * chore: fix incorrect deps (#970) * feat: support show lunar day (#972) * feat: support show lunar day * fix: update yarn.lock * fix: types error Co-authored-by: Dunqing <dengqing0821@gmail.com> * fix(ci): remove platforms * chore: improve ci (#976) * test: ci * fix: ci error * fix: on pull_request * fix: paths * fix: paths 'packages/**' * feat: support show lunar day in week and day (#977) Co-authored-by: qing <qing@imideMacBook-Air.local> * fix(client/formula): set cursor focus on input (#959) * fix(client/formula): set cursor focus on input * fix(client/formula): when formula field name has contains the other one will case FormulaError refactor partial implementation * fix(client/table-selector-provider): make data range config effective (#960) * fix(client/table-selector-provider): make data range config effective * fix(client/table-selector): fix when save data range config filter lose * fix(client/upload): fix upload mutiple files always uploading status (#974) * Update README.md * Update README.zh-CN.md * Update README.tr-TR.md * feat(calendar): support for add/remove repeats events (#988) * feat(calendar): support for adding repeats * feat: support delete events * fix: has many same x-action * feat: update better logic * fix: exclude is not an array * fix: handle parse cron error * feat: support every_week, every_month, every_year * fix(calendar): events cannot support moment (#1017) * fix(calendar): events cannot support moment * fix: avoid manipulating raw values * feat: add excel function field (#906) * feat: add excel function field * fix: translations * fix: documentation * chore: remove console log * chore: moved generation of parser to helper function; feat: added GET_DEEP function * chore: improve tokenisation for when there are similar names; * chore: move helper to utils * fix: import from @nocobase/utils/client * feat: integration as a plugin * fix: remove excel function interface * fix: locale * fix: error * refactor: formula plugin * fix: improve code * feat: update docs * fix: error * fix: update deps * fix: remove formula * fix: bigint & test * fix: result * fix: double data type * fix: maximum call stack size exceeded * fix: improve code Co-authored-by: Junyi <mytharcher@users.noreply.github.com> Co-authored-by: arzanov <59161748+arzanov@users.noreply.github.com> Co-authored-by: ChengLei Shao <chareice@live.com> Co-authored-by: lyf-coder <58352715+lyf-coder@users.noreply.github.com> Co-authored-by: katherinehhh <shunai.tang@hand-china.com> Co-authored-by: 唐小爱 <tangxiaoai@192.168.0.103> Co-authored-by: katherinehhh <katherine_15995@163.com> Co-authored-by: altaytahsin <altay.tahsin@gmail.com> Co-authored-by: Dunqing <dengqing0821@gmail.com> Co-authored-by: qing <qing@imideMacBook-Air.local> Co-authored-by: Zhou <zhou.working@gmail.com> Co-authored-by: Michael DeGiovanni <m.degiovanni@gmail.com>
This commit is contained in:
		
							parent
							
								
									8cad307fbb
								
							
						
					
					
						commit
						0469b8c44d
					
				
							
								
								
									
										1
									
								
								packages/app/client/src/plugins/excel-formula-field.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/app/client/src/plugins/excel-formula-field.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from '@nocobase/plugin-excel-formula-field/client';
 | 
				
			||||||
							
								
								
									
										1
									
								
								packages/app/client/src/plugins/math-formula-field.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/app/client/src/plugins/math-formula-field.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from '@nocobase/plugin-math-formula-field/client';
 | 
				
			||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { Spin } from 'antd';
 | 
					import { Spin } from 'antd';
 | 
				
			||||||
import React, { useState } from 'react';
 | 
					import React, { useContext, useState } from 'react';
 | 
				
			||||||
import { useAPIClient, useRequest } from '../api-client';
 | 
					import { useAPIClient, useRequest } from '../api-client';
 | 
				
			||||||
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
 | 
					import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
 | 
				
			||||||
import { CollectionManagerContext } from './context';
 | 
					import { CollectionManagerContext } from './context';
 | 
				
			||||||
@ -8,11 +8,13 @@ import { CollectionManagerOptions } from './types';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => {
 | 
					export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => {
 | 
				
			||||||
  const { service, interfaces, collections = [], refreshCM } = props;
 | 
					  const { service, interfaces, collections = [], refreshCM } = props;
 | 
				
			||||||
 | 
					  const ctx = useContext(CollectionManagerContext);
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <CollectionManagerContext.Provider
 | 
					    <CollectionManagerContext.Provider
 | 
				
			||||||
      value={{
 | 
					      value={{
 | 
				
			||||||
 | 
					        ...ctx,
 | 
				
			||||||
        service,
 | 
					        service,
 | 
				
			||||||
        interfaces: { ...defaultInterfaces, ...interfaces },
 | 
					        interfaces: { ...defaultInterfaces, ...interfaces, ...ctx.interfaces },
 | 
				
			||||||
        collections,
 | 
					        collections,
 | 
				
			||||||
        refreshCM,
 | 
					        refreshCM,
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { PlusOutlined } from '@ant-design/icons';
 | 
					import { PlusOutlined } from '@ant-design/icons';
 | 
				
			||||||
import { ArrayTable } from '@formily/antd';
 | 
					import { ArrayTable } from '@formily/antd';
 | 
				
			||||||
import { ISchema, useForm } from '@formily/react';
 | 
					import { useForm } from '@formily/react';
 | 
				
			||||||
import { uid } from '@formily/shared';
 | 
					import { uid } from '@formily/shared';
 | 
				
			||||||
import { Button, Dropdown, Menu } from 'antd';
 | 
					import { Button, Dropdown, Menu } from 'antd';
 | 
				
			||||||
import { cloneDeep } from 'lodash';
 | 
					import { cloneDeep } from 'lodash';
 | 
				
			||||||
@ -14,9 +14,9 @@ import { useCollectionManager } from '../hooks';
 | 
				
			|||||||
import { IField } from '../interfaces/types';
 | 
					import { IField } from '../interfaces/types';
 | 
				
			||||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
 | 
					import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
 | 
				
			||||||
import * as components from './components';
 | 
					import * as components from './components';
 | 
				
			||||||
import { options } from './interfaces';
 | 
					import { getOptions } from './interfaces';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getSchema = (schema: IField, record: any, compile): ISchema => {
 | 
					const getSchema = (schema: IField, record: any, compile) => {
 | 
				
			||||||
  if (!schema) {
 | 
					  if (!schema) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -24,7 +24,7 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
 | 
				
			|||||||
  const properties = cloneDeep(schema.properties) as any;
 | 
					  const properties = cloneDeep(schema.properties) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (schema.hasDefaultValue === true) {
 | 
					  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']['title'] = compile('{{ t("Default value") }}');
 | 
				
			||||||
    properties['defaultValue']['x-decorator'] = 'FormItem';
 | 
					    properties['defaultValue']['x-decorator'] = 'FormItem';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -108,9 +108,9 @@ export const useCollectionFieldFormValues = () => {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      delete values.autoCreateReverseField;
 | 
					      delete values.autoCreateReverseField;
 | 
				
			||||||
      return values;
 | 
					      return values;
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const useCreateCollectionField = () => {
 | 
					const useCreateCollectionField = () => {
 | 
				
			||||||
  const form = useForm();
 | 
					  const form = useForm();
 | 
				
			||||||
@ -143,7 +143,7 @@ export const AddCollectionField = (props) => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const AddFieldAction = (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 { getInterface } = useCollectionManager();
 | 
				
			||||||
  const [visible, setVisible] = useState(false);
 | 
					  const [visible, setVisible] = useState(false);
 | 
				
			||||||
  const [schema, setSchema] = useState({});
 | 
					  const [schema, setSchema] = useState({});
 | 
				
			||||||
@ -164,11 +164,13 @@ export const AddFieldAction = (props) => {
 | 
				
			|||||||
              }}
 | 
					              }}
 | 
				
			||||||
              onClick={(info) => {
 | 
					              onClick={(info) => {
 | 
				
			||||||
                const schema = getSchema(getInterface(info.key), record, compile);
 | 
					                const schema = getSchema(getInterface(info.key), record, compile);
 | 
				
			||||||
 | 
					                if (schema) {
 | 
				
			||||||
                  setSchema(schema);
 | 
					                  setSchema(schema);
 | 
				
			||||||
                  setVisible(true);
 | 
					                  setVisible(true);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              {options.map((option) => {
 | 
					              {getOptions().map((option) => {
 | 
				
			||||||
                return (
 | 
					                return (
 | 
				
			||||||
                  option.children.length > 0 && (
 | 
					                  option.children.length > 0 && (
 | 
				
			||||||
                    <Menu.ItemGroup key={option.label} title={compile(option.label)}>
 | 
					                    <Menu.ItemGroup key={option.label} title={compile(option.label)}>
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					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>;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -31,7 +31,8 @@ registerGroupLabel('advanced', '{{t("Advanced type")}}');
 | 
				
			|||||||
registerGroupLabel('systemInfo', '{{t("System info")}}');
 | 
					registerGroupLabel('systemInfo', '{{t("System info")}}');
 | 
				
			||||||
registerGroupLabel('others', '{{t("Others")}}');
 | 
					registerGroupLabel('others', '{{t("Others")}}');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const options = Object.keys(groupLabels).map((groupName) => {
 | 
					export const getOptions = () => {
 | 
				
			||||||
 | 
					  return Object.keys(groupLabels).map((groupName) => {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      label: groupLabels[groupName],
 | 
					      label: groupLabels[groupName],
 | 
				
			||||||
      children: Object.keys(fields[groupName] || {})
 | 
					      children: Object.keys(fields[groupName] || {})
 | 
				
			||||||
@ -46,4 +47,7 @@ export const options = Object.keys(groupLabels).map((groupName) => {
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
        .sort((a, b) => a.order - b.order),
 | 
					        .sort((a, b) => a.order - b.order),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
});
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const options = getOptions();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { ISchema } from '@formily/react';
 | 
					import { ISchema } from '@formily/react';
 | 
				
			||||||
import { CollectionOptions } from '../../types';
 | 
					import { CollectionOptions } from '../../types';
 | 
				
			||||||
 | 
					import { CollectionFieldInterface } from '../components/CollectionFieldInterface';
 | 
				
			||||||
import { options } from '../interfaces';
 | 
					import { options } from '../interfaces';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const collection: CollectionOptions = {
 | 
					const collection: CollectionOptions = {
 | 
				
			||||||
@ -155,9 +156,10 @@ export const collectionFieldSchema: ISchema = {
 | 
				
			|||||||
          type: 'void',
 | 
					          type: 'void',
 | 
				
			||||||
          'x-decorator': 'Table.Column.Decorator',
 | 
					          'x-decorator': 'Table.Column.Decorator',
 | 
				
			||||||
          'x-component': 'Table.Column',
 | 
					          'x-component': 'Table.Column',
 | 
				
			||||||
 | 
					          title: '{{t("Field interface")}}',
 | 
				
			||||||
          properties: {
 | 
					          properties: {
 | 
				
			||||||
            interface: {
 | 
					            interface: {
 | 
				
			||||||
              'x-component': 'CollectionField',
 | 
					              'x-component': CollectionFieldInterface,
 | 
				
			||||||
              'x-read-pretty': true,
 | 
					              'x-read-pretty': true,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
				
			|||||||
@ -106,7 +106,7 @@ export const useCollectionManager = () => {
 | 
				
			|||||||
      if (!fieldNames?.length) {
 | 
					      if (!fieldNames?.length) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      let cName = collectionName;
 | 
					      let cName: any = collectionName;
 | 
				
			||||||
      let collectionField;
 | 
					      let collectionField;
 | 
				
			||||||
      while (cName && fieldNames.length > 0) {
 | 
					      while (cName && fieldNames.length > 0) {
 | 
				
			||||||
        const fileName = fieldNames.shift();
 | 
					        const fileName = fieldNames.shift();
 | 
				
			||||||
 | 
				
			|||||||
@ -5,10 +5,12 @@ export * from './CollectionManagerProvider';
 | 
				
			|||||||
export * from './CollectionManagerSchemaComponentProvider';
 | 
					export * from './CollectionManagerSchemaComponentProvider';
 | 
				
			||||||
export * from './CollectionManagerShortcut';
 | 
					export * from './CollectionManagerShortcut';
 | 
				
			||||||
export * from './CollectionProvider';
 | 
					export * from './CollectionProvider';
 | 
				
			||||||
 | 
					export * from './Configuration';
 | 
				
			||||||
 | 
					export { registerField, registerGroupLabel } from './Configuration/interfaces';
 | 
				
			||||||
export * from './context';
 | 
					export * from './context';
 | 
				
			||||||
export * from './hooks';
 | 
					export * from './hooks';
 | 
				
			||||||
 | 
					export * as interfacesProperties from './interfaces/properties';
 | 
				
			||||||
 | 
					export * from './interfaces/types';
 | 
				
			||||||
export * from './ResourceActionProvider';
 | 
					export * from './ResourceActionProvider';
 | 
				
			||||||
export * from './types';
 | 
					export * from './types';
 | 
				
			||||||
export * from './Configuration';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,8 +6,6 @@ export * from './createdAt';
 | 
				
			|||||||
export * from './createdBy';
 | 
					export * from './createdBy';
 | 
				
			||||||
export * from './datetime';
 | 
					export * from './datetime';
 | 
				
			||||||
export * from './email';
 | 
					export * from './email';
 | 
				
			||||||
export * from './formula';
 | 
					 | 
				
			||||||
export * from './sequence';
 | 
					 | 
				
			||||||
export * from './icon';
 | 
					export * from './icon';
 | 
				
			||||||
export * from './id';
 | 
					export * from './id';
 | 
				
			||||||
export * from './input';
 | 
					export * from './input';
 | 
				
			||||||
@ -27,8 +25,10 @@ export * from './phone';
 | 
				
			|||||||
export * from './radioGroup';
 | 
					export * from './radioGroup';
 | 
				
			||||||
export * from './richText';
 | 
					export * from './richText';
 | 
				
			||||||
export * from './select';
 | 
					export * from './select';
 | 
				
			||||||
 | 
					export * from './sequence';
 | 
				
			||||||
export * from './subTable';
 | 
					export * from './subTable';
 | 
				
			||||||
export * from './textarea';
 | 
					export * from './textarea';
 | 
				
			||||||
export * from './time';
 | 
					export * from './time';
 | 
				
			||||||
export * from './updatedAt';
 | 
					export * from './updatedAt';
 | 
				
			||||||
export * from './updatedBy';
 | 
					export * from './updatedBy';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,7 @@
 | 
				
			|||||||
 | 
					import { registerValidateFormats } from '@formily/core';
 | 
				
			||||||
 | 
					import { i18n } from '../../i18n';
 | 
				
			||||||
import { defaultProps, operators, unique } from './properties';
 | 
					import { defaultProps, operators, unique } from './properties';
 | 
				
			||||||
import { IField } from './types';
 | 
					import { IField } from './types';
 | 
				
			||||||
import { i18n } from '../../i18n';
 | 
					 | 
				
			||||||
import { registerValidateFormats } from '@formily/core';
 | 
					 | 
				
			||||||
import { ISchema } from '@formily/react';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
registerValidateFormats({
 | 
					registerValidateFormats({
 | 
				
			||||||
  odd: /^-?\d*[13579]$/,
 | 
					  odd: /^-?\d*[13579]$/,
 | 
				
			||||||
@ -17,7 +16,7 @@ export const integer: IField = {
 | 
				
			|||||||
  title: '{{t("Integer")}}',
 | 
					  title: '{{t("Integer")}}',
 | 
				
			||||||
  sortable: true,
 | 
					  sortable: true,
 | 
				
			||||||
  default: {
 | 
					  default: {
 | 
				
			||||||
    type: 'integer',
 | 
					    type: 'bigInt',
 | 
				
			||||||
    // name,
 | 
					    // name,
 | 
				
			||||||
    uiSchema: {
 | 
					    uiSchema: {
 | 
				
			||||||
      type: 'number',
 | 
					      type: 'number',
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { defaultProps, operators, unique } from './properties';
 | 
					 | 
				
			||||||
import { IField } from './types';
 | 
					 | 
				
			||||||
import { registerValidateRules } from '@formily/core';
 | 
					import { registerValidateRules } from '@formily/core';
 | 
				
			||||||
 | 
					import { defaultProps } from './properties';
 | 
				
			||||||
 | 
					import { IField } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
registerValidateRules({
 | 
					registerValidateRules({
 | 
				
			||||||
  json(value) {
 | 
					  json(value) {
 | 
				
			||||||
@ -20,7 +20,7 @@ export const json: IField = {
 | 
				
			|||||||
  name: 'json',
 | 
					  name: 'json',
 | 
				
			||||||
  type: 'object',
 | 
					  type: 'object',
 | 
				
			||||||
  group: 'advanced',
 | 
					  group: 'advanced',
 | 
				
			||||||
  order: 3,
 | 
					  order: 4,
 | 
				
			||||||
  title: '{{t("JSON")}}',
 | 
					  title: '{{t("JSON")}}',
 | 
				
			||||||
  sortable: true,
 | 
					  sortable: true,
 | 
				
			||||||
  default: {
 | 
					  default: {
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ export const type: ISchema = {
 | 
				
			|||||||
    { label: 'String', value: 'string' },
 | 
					    { label: 'String', value: 'string' },
 | 
				
			||||||
    { label: 'Text', value: 'text' },
 | 
					    { label: 'Text', value: 'text' },
 | 
				
			||||||
    { label: 'Integer', value: 'integer' },
 | 
					    { label: 'Integer', value: 'integer' },
 | 
				
			||||||
 | 
					    { label: 'BigInteger', value: 'bigInt' },
 | 
				
			||||||
    { label: 'Float', value: 'float' },
 | 
					    { label: 'Float', value: 'float' },
 | 
				
			||||||
    { label: 'Double', value: 'double' },
 | 
					    { label: 'Double', value: 'double' },
 | 
				
			||||||
    { label: 'Decimal', value: 'decimal' },
 | 
					    { label: 'Decimal', value: 'decimal' },
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,14 @@
 | 
				
			|||||||
import React, { useContext } from 'react';
 | 
					import { css } from '@emotion/css';
 | 
				
			||||||
import { Button, Select } from 'antd';
 | 
					import { ArrayTable, FormButtonGroup, FormDrawer, FormLayout, Submit } from '@formily/antd';
 | 
				
			||||||
import { onFieldValueChange } from '@formily/core';
 | 
					import { onFieldValueChange } from '@formily/core';
 | 
				
			||||||
import { SchemaOptionsContext, useForm, useFormEffects } from '@formily/react';
 | 
					import { SchemaOptionsContext, useForm, useFormEffects } from '@formily/react';
 | 
				
			||||||
import { ArrayTable, FormButtonGroup, FormDrawer, FormLayout, Submit } from '@formily/antd';
 | 
					import { Button, Select } from 'antd';
 | 
				
			||||||
 | 
					import React, { useContext } from 'react';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { css } from '@emotion/css';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Cron, SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
 | 
					import { Cron, SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
 | 
				
			||||||
import { IField } from './types';
 | 
					 | 
				
			||||||
import { defaultProps, operators, unique } from './properties';
 | 
					import { defaultProps, operators, unique } from './properties';
 | 
				
			||||||
 | 
					import { IField } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function RuleTypeSelect(props) {
 | 
					function RuleTypeSelect(props) {
 | 
				
			||||||
  const compile = useCompile();
 | 
					  const compile = useCompile();
 | 
				
			||||||
@ -252,7 +252,7 @@ export const sequence: IField = {
 | 
				
			|||||||
  name: 'sequence',
 | 
					  name: 'sequence',
 | 
				
			||||||
  type: 'object',
 | 
					  type: 'object',
 | 
				
			||||||
  group: 'advanced',
 | 
					  group: 'advanced',
 | 
				
			||||||
  order: 2,
 | 
					  order: 3,
 | 
				
			||||||
  title: '{{t("Sequence")}}',
 | 
					  title: '{{t("Sequence")}}',
 | 
				
			||||||
  sortable: true,
 | 
					  sortable: true,
 | 
				
			||||||
  default: {
 | 
					  default: {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										224
									
								
								packages/core/client/src/formula/Expression.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								packages/core/client/src/formula/Expression.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,224 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
							
								
								
									
										53
									
								
								packages/core/client/src/formula/Result.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								packages/core/client/src/formula/Result.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 <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;
 | 
				
			||||||
							
								
								
									
										51
									
								
								packages/core/client/src/formula/demos/demo2.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								packages/core/client/src/formula/demos/demo2.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
				
			||||||
 | 
					    <SchemaComponentProvider components={{ Expression }}>
 | 
				
			||||||
 | 
					      <SchemaComponent schema={schema} />
 | 
				
			||||||
 | 
					    </SchemaComponentProvider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										10
									
								
								packages/core/client/src/formula/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								packages/core/client/src/formula/index.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					nav:
 | 
				
			||||||
 | 
					  path: /client
 | 
				
			||||||
 | 
					group:
 | 
				
			||||||
 | 
					  path: /client
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Formula
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<code src="./demos/demo2.tsx"/>
 | 
				
			||||||
							
								
								
									
										9
									
								
								packages/core/client/src/formula/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/core/client/src/formula/index.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
				
			||||||
@ -11,6 +11,7 @@ export * from './board';
 | 
				
			|||||||
export * from './china-region';
 | 
					export * from './china-region';
 | 
				
			||||||
export * from './collection-manager';
 | 
					export * from './collection-manager';
 | 
				
			||||||
export * from './document-title';
 | 
					export * from './document-title';
 | 
				
			||||||
 | 
					export * from './formula';
 | 
				
			||||||
export * from './i18n';
 | 
					export * from './i18n';
 | 
				
			||||||
export * from './icon';
 | 
					export * from './icon';
 | 
				
			||||||
export * from './plugin-manager';
 | 
					export * from './plugin-manager';
 | 
				
			||||||
 | 
				
			|||||||
@ -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 <InputNumber {...others} readOnly stringMode={true} />;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const Compute = connect(AntdCompute, mapReadPretty(ReadPretty));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Compute;
 | 
					 | 
				
			||||||
@ -1,121 +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<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;
 | 
					 | 
				
			||||||
@ -1,6 +0,0 @@
 | 
				
			|||||||
import { Compute } from './Compute';
 | 
					 | 
				
			||||||
import { FormulaInput } from './FormulaInput';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const Formula: any = () => null;
 | 
					 | 
				
			||||||
Formula.Expression = FormulaInput;
 | 
					 | 
				
			||||||
Formula.Result = Compute;
 | 
					 | 
				
			||||||
@ -4,14 +4,13 @@ export * from './calendar';
 | 
				
			|||||||
export * from './card-item';
 | 
					export * from './card-item';
 | 
				
			||||||
export * from './cascader';
 | 
					export * from './cascader';
 | 
				
			||||||
export * from './checkbox';
 | 
					export * from './checkbox';
 | 
				
			||||||
export * from './cron';
 | 
					 | 
				
			||||||
export * from './color-select';
 | 
					export * from './color-select';
 | 
				
			||||||
 | 
					export * from './cron';
 | 
				
			||||||
export * from './date-picker';
 | 
					export * from './date-picker';
 | 
				
			||||||
export * from './filter';
 | 
					export * from './filter';
 | 
				
			||||||
export * from './form';
 | 
					export * from './form';
 | 
				
			||||||
export * from './form-item';
 | 
					export * from './form-item';
 | 
				
			||||||
export * from './form-v2';
 | 
					export * from './form-v2';
 | 
				
			||||||
export * from './formula-input';
 | 
					 | 
				
			||||||
export * from './g2plot';
 | 
					export * from './g2plot';
 | 
				
			||||||
export * from './grid';
 | 
					export * from './grid';
 | 
				
			||||||
export * from './icon-picker';
 | 
					export * from './icon-picker';
 | 
				
			||||||
 | 
				
			|||||||
@ -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<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()));
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -5,12 +5,12 @@ import {
 | 
				
			|||||||
  ModelIndexesOptions,
 | 
					  ModelIndexesOptions,
 | 
				
			||||||
  QueryInterfaceOptions,
 | 
					  QueryInterfaceOptions,
 | 
				
			||||||
  SyncOptions,
 | 
					  SyncOptions,
 | 
				
			||||||
  Transactionable,
 | 
					  Transactionable
 | 
				
			||||||
} from 'sequelize';
 | 
					} from 'sequelize';
 | 
				
			||||||
import { Collection } from '../collection';
 | 
					import { Collection } from '../collection';
 | 
				
			||||||
import { Database } from '../database';
 | 
					import { Database } from '../database';
 | 
				
			||||||
import { ModelEventTypes } from '../types';
 | 
					 | 
				
			||||||
import { InheritedCollection } from '../inherited-collection';
 | 
					import { InheritedCollection } from '../inherited-collection';
 | 
				
			||||||
 | 
					import { ModelEventTypes } from '../types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface FieldContext {
 | 
					export interface FieldContext {
 | 
				
			||||||
  database: Database;
 | 
					  database: Database;
 | 
				
			||||||
 | 
				
			|||||||
@ -17,6 +17,7 @@ import {
 | 
				
			|||||||
} from './number-field';
 | 
					} from './number-field';
 | 
				
			||||||
import { PasswordFieldOptions } from './password-field';
 | 
					import { PasswordFieldOptions } from './password-field';
 | 
				
			||||||
import { RadioFieldOptions } from './radio-field';
 | 
					import { RadioFieldOptions } from './radio-field';
 | 
				
			||||||
 | 
					import { SequenceFieldOptions } from './sequence-field';
 | 
				
			||||||
import { SortFieldOptions } from './sort-field';
 | 
					import { SortFieldOptions } from './sort-field';
 | 
				
			||||||
import { StringFieldOptions } from './string-field';
 | 
					import { StringFieldOptions } from './string-field';
 | 
				
			||||||
import { TextFieldOptions } from './text-field';
 | 
					import { TextFieldOptions } from './text-field';
 | 
				
			||||||
@ -24,8 +25,6 @@ import { TimeFieldOptions } from './time-field';
 | 
				
			|||||||
import { UidFieldOptions } from './uid-field';
 | 
					import { UidFieldOptions } from './uid-field';
 | 
				
			||||||
import { UUIDFieldOptions } from './uuid-field';
 | 
					import { UUIDFieldOptions } from './uuid-field';
 | 
				
			||||||
import { VirtualFieldOptions } from './virtual-field';
 | 
					import { VirtualFieldOptions } from './virtual-field';
 | 
				
			||||||
import { FormulaFieldOptions } from './formula-field';
 | 
					 | 
				
			||||||
import { SequenceFieldOptions } from './sequence-field';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export * from './array-field';
 | 
					export * from './array-field';
 | 
				
			||||||
export * from './belongs-to-field';
 | 
					export * from './belongs-to-field';
 | 
				
			||||||
@ -41,6 +40,7 @@ export * from './number-field';
 | 
				
			|||||||
export * from './password-field';
 | 
					export * from './password-field';
 | 
				
			||||||
export * from './radio-field';
 | 
					export * from './radio-field';
 | 
				
			||||||
export * from './relation-field';
 | 
					export * from './relation-field';
 | 
				
			||||||
 | 
					export { SequenceField } from './sequence-field';
 | 
				
			||||||
export * from './sort-field';
 | 
					export * from './sort-field';
 | 
				
			||||||
export * from './string-field';
 | 
					export * from './string-field';
 | 
				
			||||||
export * from './text-field';
 | 
					export * from './text-field';
 | 
				
			||||||
@ -48,8 +48,6 @@ export * from './time-field';
 | 
				
			|||||||
export * from './uid-field';
 | 
					export * from './uid-field';
 | 
				
			||||||
export * from './uuid-field';
 | 
					export * from './uuid-field';
 | 
				
			||||||
export * from './virtual-field';
 | 
					export * from './virtual-field';
 | 
				
			||||||
export * from './formula-field';
 | 
					 | 
				
			||||||
export { SequenceField } from './sequence-field';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type FieldOptions =
 | 
					export type FieldOptions =
 | 
				
			||||||
  | BaseFieldOptions
 | 
					  | BaseFieldOptions
 | 
				
			||||||
@ -66,7 +64,6 @@ export type FieldOptions =
 | 
				
			|||||||
  | SortFieldOptions
 | 
					  | SortFieldOptions
 | 
				
			||||||
  | TextFieldOptions
 | 
					  | TextFieldOptions
 | 
				
			||||||
  | VirtualFieldOptions
 | 
					  | VirtualFieldOptions
 | 
				
			||||||
  | FormulaFieldOptions
 | 
					 | 
				
			||||||
  | ArrayFieldOptions
 | 
					  | ArrayFieldOptions
 | 
				
			||||||
  | TimeFieldOptions
 | 
					  | TimeFieldOptions
 | 
				
			||||||
  | DateFieldOptions
 | 
					  | DateFieldOptions
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								packages/plugins/excel-formula-field/client.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								packages/plugins/excel-formula-field/client.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					// @ts-nocheck
 | 
				
			||||||
 | 
					export * from './lib/client';
 | 
				
			||||||
 | 
					export { default } from './lib/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								packages/plugins/excel-formula-field/client.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								packages/plugins/excel-formula-field/client.js
									
									
									
									
									
										Executable file
									
								
							@ -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];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										12
									
								
								packages/plugins/excel-formula-field/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/plugins/excel-formula-field/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "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"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								packages/plugins/excel-formula-field/server.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								packages/plugins/excel-formula-field/server.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					// @ts-nocheck
 | 
				
			||||||
 | 
					export * from './lib/server';
 | 
				
			||||||
 | 
					export { default } from './lib/server';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								packages/plugins/excel-formula-field/server.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								packages/plugins/excel-formula-field/server.js
									
									
									
									
									
										Executable file
									
								
							@ -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];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -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,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					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,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										17
									
								
								packages/plugins/excel-formula-field/src/client/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								packages/plugins/excel-formula-field/src/client/index.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										1
									
								
								packages/plugins/excel-formula-field/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/excel-formula-field/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from './server';
 | 
				
			||||||
@ -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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								packages/plugins/excel-formula-field/src/server/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/excel-formula-field/src/server/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from './plugin';
 | 
				
			||||||
							
								
								
									
										24
									
								
								packages/plugins/excel-formula-field/src/server/plugin.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/plugins/excel-formula-field/src/server/plugin.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
				
			||||||
@ -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));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								packages/plugins/math-formula-field/client.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								packages/plugins/math-formula-field/client.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					// @ts-nocheck
 | 
				
			||||||
 | 
					export * from './lib/client';
 | 
				
			||||||
 | 
					export { default } from './lib/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								packages/plugins/math-formula-field/client.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								packages/plugins/math-formula-field/client.js
									
									
									
									
									
										Executable file
									
								
							@ -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];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										12
									
								
								packages/plugins/math-formula-field/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/plugins/math-formula-field/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "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"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								packages/plugins/math-formula-field/server.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								packages/plugins/math-formula-field/server.d.ts
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					// @ts-nocheck
 | 
				
			||||||
 | 
					export * from './lib/server';
 | 
				
			||||||
 | 
					export { default } from './lib/server';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								packages/plugins/math-formula-field/server.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								packages/plugins/math-formula-field/server.js
									
									
									
									
									
										Executable file
									
								
							@ -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];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -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,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										17
									
								
								packages/plugins/math-formula-field/src/client/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								packages/plugins/math-formula-field/src/client/index.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -1,22 +1,22 @@
 | 
				
			|||||||
import { defaultProps, operators } from './properties';
 | 
					import { IField, interfacesProperties } from '@nocobase/client';
 | 
				
			||||||
import { IField } from './types';
 | 
					const { defaultProps, operators } = interfacesProperties;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const formula: IField = {
 | 
					export const mathFormula: IField = {
 | 
				
			||||||
  name: 'formula',
 | 
					  name: 'mathFormula',
 | 
				
			||||||
  type: 'object',
 | 
					  type: 'object',
 | 
				
			||||||
  group: 'advanced',
 | 
					  group: 'advanced',
 | 
				
			||||||
  order: 1,
 | 
					  order: 1,
 | 
				
			||||||
  title: '{{t("Formula")}}',
 | 
					  title: '{{t("Math formula")}}',
 | 
				
			||||||
  description: '{{t("Formula description")}}',
 | 
					  description: '{{t("Compute a value based on the other fields using mathjs")}}',
 | 
				
			||||||
  sortable: true,
 | 
					  sortable: true,
 | 
				
			||||||
  default: {
 | 
					  default: {
 | 
				
			||||||
    type: 'formula',
 | 
					    type: 'mathFormula',
 | 
				
			||||||
    // name,
 | 
					    // name,
 | 
				
			||||||
    uiSchema: {
 | 
					    uiSchema: {
 | 
				
			||||||
      type: 'number',
 | 
					      type: 'number',
 | 
				
			||||||
      // title,
 | 
					      // title,
 | 
				
			||||||
      "x-disabled": true,
 | 
					      'x-disabled': true,
 | 
				
			||||||
      'x-component': 'Formula.Result',
 | 
					      'x-component': 'MathFormula.Result',
 | 
				
			||||||
      'x-component-props': {
 | 
					      'x-component-props': {
 | 
				
			||||||
        stringMode: true,
 | 
					        stringMode: true,
 | 
				
			||||||
        step: '1',
 | 
					        step: '1',
 | 
				
			||||||
@ -25,16 +25,16 @@ export const formula: IField = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  properties: {
 | 
					  properties: {
 | 
				
			||||||
    ...defaultProps,
 | 
					    ...defaultProps,
 | 
				
			||||||
    'expression': {
 | 
					    expression: {
 | 
				
			||||||
      type: 'string',
 | 
					      type: 'string',
 | 
				
			||||||
      title: '{{t("Expression")}}',
 | 
					      title: '{{t("Expression")}}',
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
      description: '{{t("Input +, -, *, /, ( ) to calculate, input @ to open field variables.")}}',
 | 
					      description: '{{t("Input +, -, *, /, ( ) to calculate, input @ to open field variables.")}}',
 | 
				
			||||||
      'x-component': 'Formula.Expression',
 | 
					      'x-component': 'MathFormula.Expression',
 | 
				
			||||||
      'x-decorator': 'FormItem',
 | 
					      'x-decorator': 'FormItem',
 | 
				
			||||||
      'x-component-props': {
 | 
					      'x-component-props': {
 | 
				
			||||||
        'supports': ['number', 'percent', 'integer'],
 | 
					        supports: ['number', 'percent', 'integer'],
 | 
				
			||||||
        'useCurrentFields': '{{ useCurrentFields }}'
 | 
					        useCurrentFields: '{{ useCurrentFields }}',
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    'uiSchema.x-component-props.step': {
 | 
					    'uiSchema.x-component-props.step': {
 | 
				
			||||||
							
								
								
									
										1
									
								
								packages/plugins/math-formula-field/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/math-formula-field/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from './server';
 | 
				
			||||||
@ -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<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);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										1
									
								
								packages/plugins/math-formula-field/src/server/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/plugins/math-formula-field/src/server/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export { default } from './plugin';
 | 
				
			||||||
@ -1,16 +1,17 @@
 | 
				
			|||||||
import { DataTypes } from 'sequelize';
 | 
					import { BaseFieldOptions, Field } from '@nocobase/database';
 | 
				
			||||||
import { BaseColumnFieldOptions, Field } from './field';
 | 
					 | 
				
			||||||
import * as math from 'mathjs';
 | 
					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() {
 | 
					  get dataType() {
 | 
				
			||||||
    return DataTypes.FLOAT;
 | 
					    return DataTypes.DOUBLE;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  caculate(expression, scope) {
 | 
					  calculate(expression, scope) {
 | 
				
			||||||
    let result = null;
 | 
					    let result: any = null;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      result = math.evaluate(expression, scope);
 | 
					      result = evaluate(expression, scope);
 | 
				
			||||||
      result = math.round(result, 9);
 | 
					      result = math.round(result, 9);
 | 
				
			||||||
    } catch {}
 | 
					    } catch {}
 | 
				
			||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
@ -26,7 +27,7 @@ export class FormulaField extends Field {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    for (const record of records) {
 | 
					    for (const record of records) {
 | 
				
			||||||
      const scope = record.toJSON();
 | 
					      const scope = record.toJSON();
 | 
				
			||||||
      const result = this.caculate(expression, scope);
 | 
					      const result = this.calculate(expression, scope);
 | 
				
			||||||
      if (result) {
 | 
					      if (result) {
 | 
				
			||||||
        await record.update(
 | 
					        await record.update(
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
@ -42,12 +43,12 @@ export class FormulaField extends Field {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  caculateField = async (instance) => {
 | 
					  calculateField = async (instance) => {
 | 
				
			||||||
    const { expression, name } = this.options;
 | 
					    const { expression, name } = this.options;
 | 
				
			||||||
    const scope = instance.toJSON();
 | 
					    const scope = instance.toJSON();
 | 
				
			||||||
    let result;
 | 
					    let result;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      result = math.evaluate(expression, scope);
 | 
					      result = evaluate(expression, scope);
 | 
				
			||||||
      result = math.round(result, 9);
 | 
					      result = math.round(result, 9);
 | 
				
			||||||
    } catch {}
 | 
					    } catch {}
 | 
				
			||||||
    if (result === 0 || result) {
 | 
					    if (result === 0 || result) {
 | 
				
			||||||
@ -67,7 +68,7 @@ export class FormulaField extends Field {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      for (const record of records) {
 | 
					      for (const record of records) {
 | 
				
			||||||
        const scope = record.toJSON();
 | 
					        const scope = record.toJSON();
 | 
				
			||||||
        const result = this.caculate(expression, scope);
 | 
					        const result = this.calculate(expression, scope);
 | 
				
			||||||
        await record.update(
 | 
					        await record.update(
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            [name]: result,
 | 
					            [name]: result,
 | 
				
			||||||
@ -87,20 +88,19 @@ export class FormulaField extends Field {
 | 
				
			|||||||
    this.on('afterSync', this.initFieldData);
 | 
					    this.on('afterSync', this.initFieldData);
 | 
				
			||||||
    // TODO: should not depends on fields table (which is defined by other plugin)
 | 
					    // TODO: should not depends on fields table (which is defined by other plugin)
 | 
				
			||||||
    this.database.on('fields.afterUpdate', this.updateFieldData);
 | 
					    this.database.on('fields.afterUpdate', this.updateFieldData);
 | 
				
			||||||
    this.on('beforeSave', this.caculateField);
 | 
					    this.on('beforeSave', this.calculateField);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  unbind() {
 | 
					  unbind() {
 | 
				
			||||||
    super.unbind();
 | 
					    super.unbind();
 | 
				
			||||||
    this.off('beforeSave', this.caculateField);
 | 
					    this.off('beforeSave', this.calculateField);
 | 
				
			||||||
    // TODO: should not depends on fields table
 | 
					    // TODO: should not depends on fields table
 | 
				
			||||||
    this.database.off('fields.afterUpdate', this.updateFieldData);
 | 
					    this.database.off('fields.afterUpdate', this.updateFieldData);
 | 
				
			||||||
    this.off('afterSync', this.initFieldData);
 | 
					    this.off('afterSync', this.initFieldData);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface FormulaFieldOptions extends BaseColumnFieldOptions {
 | 
					export interface MathFormulaFieldOptions extends BaseFieldOptions {
 | 
				
			||||||
  type: 'formula';
 | 
					  type: 'mathFormula';
 | 
				
			||||||
 | 
					 | 
				
			||||||
  expression: string;
 | 
					  expression: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								packages/plugins/math-formula-field/src/server/plugin.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/plugins/math-formula-field/src/server/plugin.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
@ -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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -22,6 +22,8 @@
 | 
				
			|||||||
    "@nocobase/plugin-import": "0.8.0-alpha.13",
 | 
					    "@nocobase/plugin-import": "0.8.0-alpha.13",
 | 
				
			||||||
    "@nocobase/plugin-sample-hello": "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-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-routes-storage": "0.8.0-alpha.13",
 | 
				
			||||||
    "@nocobase/plugin-ui-schema-storage": "0.8.0-alpha.13",
 | 
					    "@nocobase/plugin-ui-schema-storage": "0.8.0-alpha.13",
 | 
				
			||||||
    "@nocobase/plugin-users": "0.8.0-alpha.13",
 | 
					    "@nocobase/plugin-users": "0.8.0-alpha.13",
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,8 @@ export class PresetNocoBase extends Plugin {
 | 
				
			|||||||
      'export',
 | 
					      'export',
 | 
				
			||||||
      'import',
 | 
					      'import',
 | 
				
			||||||
      'audit-logs',
 | 
					      'audit-logs',
 | 
				
			||||||
 | 
					      'math-formula-field',
 | 
				
			||||||
 | 
					      'excel-formula-field',
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
    await this.app.pm.add(plugins, {
 | 
					    await this.app.pm.add(plugins, {
 | 
				
			||||||
      enabled: true,
 | 
					      enabled: true,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user