Merge branch 'main' into next

This commit is contained in:
nocobase[bot] 2025-03-05 14:15:53 +00:00
commit ea64d975ed
9 changed files with 98 additions and 7 deletions

View File

@ -22,7 +22,7 @@ export default evaluate.bind(function (expression: string, scope = {}) {
if (Number.isNaN(result) || !Number.isFinite(result)) { if (Number.isNaN(result) || !Number.isFinite(result)) {
return null; return null;
} }
return round(result, 9); return round(result, 14);
} }
return result; return result;
}, {}); }, {});

View File

@ -18,7 +18,7 @@ export default evaluate.bind(
if (Number.isNaN(result) || !Number.isFinite(result)) { if (Number.isNaN(result) || !Number.isFinite(result)) {
return null; return null;
} }
return math.round(result, 9); return math.round(result, 14);
} }
if (result instanceof math.Matrix) { if (result instanceof math.Matrix) {
return result.toArray(); return result.toArray();

View File

@ -11,6 +11,7 @@
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/workflow-aggregate", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/workflow-aggregate",
"devDependencies": { "devDependencies": {
"antd": "5.x", "antd": "5.x",
"mathjs": "^10.6.0",
"react": "18.x", "react": "18.x",
"react-i18next": "^11.15.1" "react-i18next": "^11.15.1"
}, },

View File

@ -338,9 +338,9 @@ export default class extends Instruction {
properties: { properties: {
distinct: { distinct: {
type: 'boolean', type: 'boolean',
title: `{{t("Distinct", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Checkbox', 'x-component': 'Checkbox',
'x-content': `{{t("Distinct", { ns: "${NAMESPACE}" })}}`,
'x-reactions': [ 'x-reactions': [
{ {
dependencies: ['collection', 'aggregator'], dependencies: ['collection', 'aggregator'],

View File

@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { round } from 'mathjs';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { import {
AggregateNode, AggregateNode,
@ -350,7 +351,7 @@ test.describe('filter', () => {
aggregateNodeCollectionData.reduce((total, currentValue) => { aggregateNodeCollectionData.reduce((total, currentValue) => {
return currentValue.staffnum > 3 ? total + currentValue.staffnum : total; return currentValue.staffnum > 3 ? total + currentValue.staffnum : total;
}, 0) / aggregateNodeCollectionDataCount; }, 0) / aggregateNodeCollectionDataCount;
expect(aggregateNodeJobResult).toBe(aggregateNodeCollectionDataAvg); expect(aggregateNodeJobResult).toBe(round(aggregateNodeCollectionDataAvg, 14));
// 4、后置处理删除工作流 // 4、后置处理删除工作流
await apiDeleteWorkflow(workflowId); await apiDeleteWorkflow(workflowId);
}); });

View File

@ -7,6 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { round } from 'mathjs';
import { parseCollectionName } from '@nocobase/data-source-manager'; import { parseCollectionName } from '@nocobase/data-source-manager';
import { DataTypes } from '@nocobase/database'; import { DataTypes } from '@nocobase/database';
import { Processor, Instruction, JOB_STATUS, FlowNodeModel } from '@nocobase/plugin-workflow'; import { Processor, Instruction, JOB_STATUS, FlowNodeModel } from '@nocobase/plugin-workflow';
@ -47,7 +49,7 @@ export default class extends Instruction {
}); });
return { return {
result: options.dataType === DataTypes.DOUBLE ? Number(result) : result, result: round(options.dataType === DataTypes.DOUBLE ? Number(result) : result, 14),
status: JOB_STATUS.RESOLVED, status: JOB_STATUS.RESOLVED,
}; };
} }

View File

@ -48,7 +48,7 @@ describe('workflow > instructions > aggregate', () => {
afterEach(() => app.destroy()); afterEach(() => app.destroy());
describe('based on collection', () => { describe('based on collection', () => {
it('count', async () => { it('count with data matched', async () => {
const n1 = await workflow.createNode({ const n1 = await workflow.createNode({
type: 'aggregate', type: 'aggregate',
config: { config: {
@ -69,6 +69,30 @@ describe('workflow > instructions > aggregate', () => {
expect(job.result).toBe(1); expect(job.result).toBe(1);
}); });
it('count without data matched', async () => {
const n1 = await workflow.createNode({
type: 'aggregate',
config: {
aggregator: 'count',
collection: 'posts',
params: {
field: 'id',
filter: {
id: 0,
},
},
},
});
const post = await PostRepo.create({ values: { title: 't1' } });
await sleep(500);
const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs();
expect(job.result).toBe(0);
});
it('sum', async () => { it('sum', async () => {
const n1 = await workflow.createNode({ const n1 = await workflow.createNode({
type: 'aggregate', type: 'aggregate',
@ -98,6 +122,64 @@ describe('workflow > instructions > aggregate', () => {
expect(j2.result).toBe(3); expect(j2.result).toBe(3);
}); });
it('sum double field', async () => {
const n1 = await workflow.createNode({
type: 'aggregate',
config: {
aggregator: 'sum',
collection: 'posts',
params: {
field: 'score',
},
},
});
const p1 = await PostRepo.create({ values: { title: 't1', score: 0.1 } });
await sleep(500);
const [e1] = await workflow.getExecutions();
const [j1] = await e1.getJobs();
expect(j1.result).toBe(0.1);
const p2 = await PostRepo.create({ values: { title: 't2', score: 0.2 } });
await sleep(500);
const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] });
const [j2] = await e2.getJobs();
expect(j2.result).toBe(0.3);
});
it('sum number will be rounded', async () => {
const n1 = await workflow.createNode({
type: 'aggregate',
config: {
aggregator: 'sum',
collection: 'posts',
params: {
field: 'score',
},
},
});
const p1 = await PostRepo.create({ values: { title: 't1', score: 0.100000000000001 } });
await sleep(500);
const [e1] = await workflow.getExecutions();
const [j1] = await e1.getJobs();
expect(j1.result).toBe(0.1);
const p2 = await PostRepo.create({ values: { title: 't2', score: 0.200000000000001 } });
await sleep(500);
const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] });
const [j2] = await e2.getJobs();
expect(j2.result).toBe(0.3);
});
it('avg', async () => { it('avg', async () => {
const n1 = await workflow.createNode({ const n1 = await workflow.createNode({
type: 'aggregate', type: 'aggregate',

View File

@ -571,7 +571,7 @@ export class AggregateNode {
.getByLabel('block-item-FieldsSelect-workflows-Field to aggregate') .getByLabel('block-item-FieldsSelect-workflows-Field to aggregate')
.locator('.ant-select-selection-search-input'); .locator('.ant-select-selection-search-input');
this.distinctCheckBox = page this.distinctCheckBox = page
.getByLabel('block-item-Checkbox-workflows-Distinct') .getByLabel('block-item-Checkbox-workflows')
.locator('input.ant-checkbox-input[type="checkbox"]'); .locator('input.ant-checkbox-input[type="checkbox"]');
this.submitButton = page.getByLabel('action-Action-Submit-workflows'); this.submitButton = page.getByLabel('action-Action-Submit-workflows');
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows'); this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');

View File

@ -45,6 +45,11 @@ export default {
name: 'read', name: 'read',
defaultValue: 0, defaultValue: 0,
}, },
{
type: 'double',
name: 'score',
defaultValue: 0,
},
{ {
type: 'date', type: 'date',
name: 'createdAt', name: 'createdAt',