diff --git a/packages/core/client/src/schema-component/antd/form-v2/utils.tsx b/packages/core/client/src/schema-component/antd/form-v2/utils.tsx index b8eef3f67..33ed629bf 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/utils.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/utils.tsx @@ -162,7 +162,7 @@ async function replaceVariables( return { exp: value.replace(REGEX_OF_VARIABLE, (match) => { - return store[match] || match; + return `{{${store[match] || match}}}`; }), scope, }; diff --git a/packages/core/evaluators/src/server/__tests__/index.test.ts b/packages/core/evaluators/src/server/__tests__/index.test.ts index aa5cd2a18..056aab05f 100644 --- a/packages/core/evaluators/src/server/__tests__/index.test.ts +++ b/packages/core/evaluators/src/server/__tests__/index.test.ts @@ -18,6 +18,15 @@ describe('evaluate', () => { expect(result).toBe("I'm done."); }); + it('function result null', () => { + const result = formulaEval('{{a}}', { + a() { + return null; + }, + }); + expect(result).toBe(null); + }); + it('function result number', () => { const result = formulaEval('{{a}}', { a() { @@ -28,16 +37,29 @@ describe('evaluate', () => { }); it('function result Date', () => { + const now = new Date(); const result = formulaEval('{{a}}', { a() { - return new Date(); + return now; }, }); - expect(result).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z/); + expect(result).toBeInstanceOf(Date); + expect(result).toBe(now); }); - it('number path to array item 0 (math.js)', () => { - expect(() => mathEval('{{a.0}}', { a: [1, 2, 3] })).toThrow(); + it('deep array', () => { + const result = formulaEval('{{a.b}}', { a: [{ b: 1 }, { b: 2 }] }); + expect(result).toEqual([1, 2]); + }); + + it('key with dash', () => { + const result = formulaEval('{{a-b}}', { 'a-b': 1 }); + expect(result).toBe(1); + }); + + it('deep key with dash', () => { + const result = formulaEval('{{a.b-c}}', { a: { 'b-c': 1 } }); + expect(result).toBe(1); }); it('number path to array item 1 (math.js)', () => { @@ -45,7 +67,12 @@ describe('evaluate', () => { expect(result).toBe(1); }); - it('number path to array item 1 (math.js)', () => { + // NOTE: This case is skipped because `a.` is not able to be configured from UI. + it.skip('number path to array item 0 (math.js)', () => { + expect(() => mathEval('{{a.0}}', { a: [1, 2, 3] })).toThrow(); + }); + + it.skip('number path to array item 1 (math.js)', () => { const result = mathEval('{{a.1}}', { a: [1, 2, 3] }); expect(result).toBe(1); }); diff --git a/packages/core/evaluators/src/utils/index.ts b/packages/core/evaluators/src/utils/index.ts index 37665ba5f..609387b75 100644 --- a/packages/core/evaluators/src/utils/index.ts +++ b/packages/core/evaluators/src/utils/index.ts @@ -17,49 +17,25 @@ export function appendArrayColumn(scope, key) { } } -function replaceNumberIndex(path: string, scope: Scope): string { - const segments = path.split('.'); - const paths: string[] = []; - - for (let i = 0; i < segments.length; i++) { - const p = segments[i]; - if (p[0] && '0123456789'.indexOf(p[0]) > -1) { - paths.push(Array.isArray(get(scope, segments.slice(0, i))) ? `[${p}]` : `["${p}"]`); - } else { - if (i) { - paths.push('.', p); - } else { - paths.push(p); - } - } - } - - return paths.join(''); -} - export function evaluate(this: Evaluator, expression: string, scope: Scope = {}) { const context = cloneDeep(scope); + const newContext = {}; const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => { appendArrayColumn(context, v); - const item = get(context, v); + let item = get(context, v); - let result; + if (typeof item === 'function') { + item = item(); + } + const randomKey = `$$${Math.random().toString(36).slice(2, 10).padEnd(8, '0')}`; if (item == null) { - result = 'null'; - } else if (typeof item === 'function') { - result = item(); - result = typeof result === 'string' ? `'${result.replace(/'/g, "\\'")}'` : result; - } else { - result = replaceNumberIndex(v, context); + return 'null'; } - - if (result instanceof Date) { - result = `'${result.toISOString()}'`; - } - - return ` ${result} `; + newContext[randomKey] = item; + return ` ${randomKey} `; }); - return this(exp, context); + + return this(exp, newContext); } diff --git a/packages/plugins/@nocobase/plugin-formula-field/src/server/formula-field.ts b/packages/plugins/@nocobase/plugin-formula-field/src/server/formula-field.ts index 1fce43568..da7144e2d 100644 --- a/packages/plugins/@nocobase/plugin-formula-field/src/server/formula-field.ts +++ b/packages/plugins/@nocobase/plugin-formula-field/src/server/formula-field.ts @@ -69,29 +69,31 @@ export class FormulaField extends Field { }; 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 } = this.options; + if (this.collection.name !== instance.collectionName || instance.name !== this.options.name) { + return; + } - const records = await this.collection.repository.find({ - order: [this.collection.model.primaryKeyAttribute], - transaction, - }); + this.options = Object.assign(this.options, instance.options); + const { name } = this.options; - for (const record of records) { - const scope = record.toJSON(); - const result = this.calculate(scope); - await record.update( - { - [name]: result, - }, - { - transaction, - silent: true, - hooks: false, - }, - ); - } + 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(scope); + await record.update( + { + [name]: result, + }, + { + transaction, + silent: true, + hooks: false, + }, + ); } }; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/calculation.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/calculation.test.ts index f3432c589..27674dca6 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/calculation.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/instructions/calculation.test.ts @@ -166,7 +166,8 @@ describe('workflow > instructions > calculation', () => { const [execution] = await workflow.getExecutions(); const [job] = await execution.getJobs(); - expect(job.result).toBe('a $context.data.title '); + expect((job.result as string).startsWith('a $$')).toBe(true); + expect((job.result as string).endsWith(' ')).toBe(true); }); it('text', async () => {