feat(automations): parallel branch canvas, simple/advanced level per org, rules list UX

This commit is contained in:
2026-10-11 14:48:12 +03:30
parent b3b92abae9
commit 17638786a8
18 changed files with 957 additions and 354 deletions
@@ -6,6 +6,7 @@ import {
} from '@nestjs/common';
import type {
AutomationAction,
AutomationChainMode,
AutomationConditionField,
AutomationConditionOp,
AutomationNotifyChannel,
@@ -19,6 +20,7 @@ import type {
import {
MAX_AUTOMATION_STEPS,
automationStepToConfig,
automationUsesAdvanced,
firstAutomationAction,
matchFormFieldCondition,
normalizeAutomationSteps,
@@ -95,6 +97,7 @@ export class AutomationsService {
const action = firstAutomationAction(steps);
if (!action) throw new BadRequestException('حداقل یک عمل لازم است');
await this.assertStepRefs(organizationId, steps);
await this.assertLevelAllows(organizationId, steps, chainExtra(dto.config).chainMode);
const row = await this.prisma.automationRule.create({
data: {
organizationId,
@@ -130,7 +133,14 @@ export class AutomationsService {
if (steps && !nextAction) {
throw new BadRequestException('حداقل یک عمل لازم است');
}
if (steps) await this.assertStepRefs(organizationId, steps);
if (steps) {
await this.assertStepRefs(organizationId, steps);
await this.assertLevelAllows(
organizationId,
steps,
chainExtra(dto.config ?? asConfig(existing.config)).chainMode,
);
}
const row = await this.prisma.automationRule.update({
where: { id },
@@ -487,6 +497,23 @@ export class AutomationsService {
}
}
private async assertLevelAllows(
organizationId: string,
steps: AutomationStep[],
chainMode: AutomationChainMode | undefined,
) {
if (!automationUsesAdvanced(steps, chainMode)) return;
const org = await this.prisma.organization.findUnique({
where: { id: organizationId },
select: { enabledModules: true },
});
if (!org?.enabledModules.includes('workflow_automations_advanced')) {
throw new BadRequestException(
'شاخه‌ها، شرط روی پاسخ فرم و تقسیم نوبتی فقط در اتوماسیون حرفه‌ای فعال است',
);
}
}
private async executeNotify(
title: string,
config: AutomationRuleConfig,
+29
View File
@@ -1,7 +1,9 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
automationUsesAdvanced,
matchFormFieldCondition,
splitAutomationBranches,
planAutomationActions,
splitFormAnswer,
type AutomationStep,
@@ -91,6 +93,33 @@ describe('اتوماسیون شاخه‌ای فرم', () => {
);
});
it('تفکیک شاخه‌ها برای نمایش موازی', () => {
assert.deepEqual(splitAutomationBranches(BSJ), {
prefixEnd: 0,
branches: [
{ start: 0, end: 2 },
{ start: 2, end: 4 },
{ start: 4, end: 6 },
],
});
const mixed = [act('a0'), cond('c1', 'x'), cond('c2', 'y'), act('a1')];
assert.deepEqual(splitAutomationBranches(mixed), {
prefixEnd: 1,
branches: [{ start: 1, end: 4 }],
});
assert.deepEqual(splitAutomationBranches([act('a0')]), { prefixEnd: 1, branches: [] });
});
it('تشخیص نیاز به اتوماسیون حرفه‌ای', () => {
assert.equal(automationUsesAdvanced([act('a0')], 'stop'), false);
assert.equal(automationUsesAdvanced([act('a0')], 'branches'), true);
assert.equal(automationUsesAdvanced(BSJ, 'stop'), true);
assert.equal(
automationUsesAdvanced([{ ...act('a0'), assignMode: 'team_round_robin' }], undefined),
true,
);
});
it('تقسیم نوبتی مساوی و پایدار', () => {
const team = ['u3', 'u1', 'u2'];
const picks = [1, 2, 3, 4, 5, 6].map((n) => pickRoundRobin(team, n));