feat(automations): multi-option form-field conditions (all / any / none / exact)

This commit is contained in:
2026-10-11 17:26:17 +03:30
parent f4398cdb3e
commit b43ed68e82
6 changed files with 93 additions and 16 deletions
+36 -7
View File
@@ -3166,6 +3166,7 @@ export type AutomationConditionOp =
| "is_set"
| "is_empty"
| "contains"
| "contains_any"
| "not_contains";
/** stop: شرط ناموفق کل زنجیره را متوقف می‌کند — branches: هر شرط فقط عمل‌های بعدی خودش را تا شرط بعدی کنترل می‌کند */
@@ -3214,6 +3215,7 @@ export const AUTOMATION_CONDITION_OP_LABELS: Record<
is_set: "دارد",
is_empty: "ندارد",
contains: "شامل",
contains_any: "شامل یکی از",
not_contains: "شامل نیست",
};
@@ -3269,7 +3271,7 @@ export function conditionOpsForField(
if (field === "deal_amount") return ["gte", "lte", "eq", "neq"];
if (field === "ticket_priority") return ["eq", "neq"];
if (field === "form_field") {
return ["contains", "not_contains", "eq", "neq", "is_set", "is_empty"];
return ["contains", "contains_any", "not_contains", "eq", "neq", "is_set", "is_empty"];
}
return ["is_set", "is_empty"];
}
@@ -3289,6 +3291,8 @@ export interface AutomationStep {
conditionField?: AutomationConditionField;
conditionOp?: AutomationConditionOp;
conditionValue?: string | number;
/** چند گزینهٔ فیلد فرم (چندانتخابی)؛ اگر پر باشد به‌جای conditionValue استفاده می‌شود */
conditionValues?: string[];
/** کلید فیلد فرم برای شرط form_field */
conditionKey?: string | null;
assignMode?: AutomationAssignMode;
@@ -3365,6 +3369,9 @@ export function normalizeAutomationSteps(
conditionField: s.conditionField,
conditionOp: s.conditionOp,
conditionValue: s.conditionValue,
conditionValues: Array.isArray(s.conditionValues)
? s.conditionValues.map((v) => String(v).trim()).filter(Boolean)
: undefined,
conditionKey: s.conditionKey,
assignMode: s.assignMode,
assigneeTeamId: s.assigneeTeamId,
@@ -3518,27 +3525,49 @@ export function splitFormAnswer(raw: unknown): string[] {
.filter(Boolean);
}
/** گزینه‌های مورد انتظار شرط فرم: چندتایی اگر پر باشد، وگرنه مقدار تکی */
export function formConditionValues(
step: Pick<AutomationStep, "conditionValue" | "conditionValues">,
): string[] {
const many = (step.conditionValues ?? []).map((v) => String(v).trim()).filter(Boolean);
if (many.length) return [...new Set(many)];
const one = String(step.conditionValue ?? "").trim();
return one ? [one] : [];
}
/**
* contains: همهٔ گزینه‌ها انتخاب شده باشند — contains_any: حداقل یکی —
* not_contains: هیچ‌کدام — eq: دقیقاً همین گزینه‌ها (نه بیشتر، نه کمتر)
*/
export function matchFormFieldCondition(
op: AutomationConditionOp | undefined,
raw: unknown,
expected: unknown,
expected: string | number | string[] | null | undefined,
): boolean {
const parts = splitFormAnswer(raw);
const value = String(expected ?? "").trim();
const whole = raw == null ? "" : String(raw).trim();
const values = (Array.isArray(expected) ? expected : [expected])
.map((v) => String(v ?? "").trim())
.filter(Boolean);
const exact = () =>
values.length === 1
? whole === values[0] || (parts.length === 1 && parts[0] === values[0])
: new Set(parts).size === values.length && values.every((v) => parts.includes(v));
switch (op ?? "contains") {
case "is_set":
return parts.length > 0;
case "is_empty":
return parts.length === 0;
case "contains":
return Boolean(value) && parts.includes(value);
return values.length > 0 && values.every((v) => parts.includes(v));
case "contains_any":
return values.some((v) => parts.includes(v));
case "not_contains":
return !value || !parts.includes(value);
return !values.some((v) => parts.includes(v));
case "eq":
return whole === value || (parts.length === 1 && parts[0] === value);
return values.length > 0 && exact();
case "neq":
return !(whole === value || (parts.length === 1 && parts[0] === value));
return !(values.length > 0 && exact());
default:
return false;
}