fix(automations): harden delayed rules against races, stale edits, and unsupported configs
- episode_key_for falls back to the incoming message's created_at when waiting_since is still nil (it is written after MESSAGE_CREATED dispatches), so awaiting-agent episodes no longer arm as awaiting_agent:0 and get skipped as episode_moved on the first customer message after an agent reply. - Editing a rule's trigger, conditions, or actions (not just the delay) now discards its armed pending rows so they can't fire against a definition they were never armed under; deleting rather than skipping frees the episode slot so the new definition re-arms on the next matching event. - The rule form only offers a delay when the config supports it: the delayed option is disabled with an explanation for attribute_changed conditions and for non-status conditions on conversation events, and the backend's specific error is surfaced on save instead of a generic message.
This commit is contained in:
@@ -38,7 +38,11 @@
|
||||
"DAYS": "Days"
|
||||
},
|
||||
"ERROR": "Delay must be between 10 minutes and 30 days",
|
||||
"HELP_TEXT": "Delayed rules run only if the conditions still hold when the delay ends, and apply to conversations with activity after the rule is created."
|
||||
"HELP_TEXT": "Delayed rules run only if the conditions still hold when the delay ends, and apply to conversations with activity after the rule is created.",
|
||||
"RESTRICTED": {
|
||||
"ATTRIBUTE_CHANGED": "A delay can't be used with an \"attribute changed\" condition, because the change can't be re-checked when the delay ends.",
|
||||
"CONVERSATION_NON_STATUS": "For conversation events, a delay is only available when every condition uses the Status attribute."
|
||||
}
|
||||
},
|
||||
"CONDITIONS": {
|
||||
"LABEL": "Conditions"
|
||||
|
||||
+56
-3
@@ -197,6 +197,30 @@ const allowsDelayedExecution = computed(() =>
|
||||
isCloudFeatureEnabled(FEATURE_FLAGS.DELAYED_AUTOMATIONS)
|
||||
);
|
||||
|
||||
// Mirrors the backend's execution_delay validations so we never offer an unsupported delay.
|
||||
const delayRestrictionReason = computed(() => {
|
||||
const conditions = automation.value?.conditions || [];
|
||||
if (
|
||||
conditions.some(
|
||||
condition => condition.filter_operator === 'attribute_changed'
|
||||
)
|
||||
) {
|
||||
return 'ATTRIBUTE_CHANGED';
|
||||
}
|
||||
if (
|
||||
eventName.value !== 'message_created' &&
|
||||
conditions.some(
|
||||
condition =>
|
||||
condition.attribute_key && condition.attribute_key !== 'status'
|
||||
)
|
||||
) {
|
||||
return 'CONVERSATION_NON_STATUS';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const isDelaySupported = computed(() => delayRestrictionReason.value === null);
|
||||
|
||||
const executeMode = ref('immediate');
|
||||
const delayValue = ref(4);
|
||||
const delayUnit = ref('HOURS');
|
||||
@@ -237,6 +261,13 @@ watch([executeMode, delayInMinutes], () => {
|
||||
executeMode.value === 'delayed' ? delayInMinutes.value : null;
|
||||
});
|
||||
|
||||
// Editing the event/conditions into an unsupported shape reverts the delay to immediate.
|
||||
watch(isDelaySupported, supported => {
|
||||
if (!supported && executeMode.value === 'delayed') {
|
||||
executeMode.value = 'immediate';
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => automation.value,
|
||||
() => {
|
||||
@@ -361,11 +392,23 @@ defineExpose({ open, close });
|
||||
<input v-model="executeMode" type="radio" value="immediate" />
|
||||
{{ $t('AUTOMATION.ADD.FORM.EXECUTE.IMMEDIATELY') }}
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
|
||||
<input v-model="executeMode" type="radio" value="delayed" />
|
||||
<label
|
||||
class="flex items-center gap-1.5 text-sm"
|
||||
:class="
|
||||
isDelaySupported
|
||||
? 'cursor-pointer'
|
||||
: 'cursor-not-allowed opacity-50'
|
||||
"
|
||||
>
|
||||
<input
|
||||
v-model="executeMode"
|
||||
type="radio"
|
||||
value="delayed"
|
||||
:disabled="!isDelaySupported"
|
||||
/>
|
||||
{{ $t('AUTOMATION.ADD.FORM.EXECUTE.AFTER_DELAY') }}
|
||||
</label>
|
||||
<template v-if="executeMode === 'delayed'">
|
||||
<template v-if="executeMode === 'delayed' && isDelaySupported">
|
||||
<input
|
||||
v-model.number="delayValue"
|
||||
type="number"
|
||||
@@ -386,6 +429,16 @@ defineExpose({ open, close });
|
||||
<span v-if="errors.execution_delay" class="text-xs text-n-ruby-9">
|
||||
{{ $t('AUTOMATION.ADD.FORM.EXECUTE.ERROR') }}
|
||||
</span>
|
||||
<p
|
||||
v-else-if="!isDelaySupported"
|
||||
class="text-xs text-n-amber-11 pt-1 mb-0"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`AUTOMATION.ADD.FORM.EXECUTE.RESTRICTED.${delayRestrictionReason}`
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p
|
||||
v-else-if="executeMode === 'delayed'"
|
||||
class="text-xs text-n-slate-11 pt-1 mb-0"
|
||||
|
||||
@@ -136,11 +136,11 @@ const submitAutomation = async (payload, mode) => {
|
||||
hideAddPopup();
|
||||
hideEditPopup();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
const fallbackMessage =
|
||||
mode === 'edit'
|
||||
? t('AUTOMATION.EDIT.API.ERROR_MESSAGE')
|
||||
: t('AUTOMATION.ADD.API.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
useAlert(error?.response?.data?.error || fallbackMessage);
|
||||
}
|
||||
};
|
||||
const toggleAutomation = async ({ id, name, status }) => {
|
||||
|
||||
Reference in New Issue
Block a user