Merge feature/cw-7513 into feature/cw-7513-specs
This commit is contained in:
@@ -68,6 +68,10 @@
|
||||
"ACTIONS": "Actions"
|
||||
},
|
||||
"404": "No automation rules found",
|
||||
"SECTIONS": {
|
||||
"INSTANT": "Automations",
|
||||
"DELAYED": "Delayed (Wait)"
|
||||
},
|
||||
"DELAY_BADGE": "Runs after {delay}",
|
||||
"DELAY_DISABLED_BANNER": "Waits are disabled for this account. Rules with a wait won't run until it is enabled again."
|
||||
},
|
||||
|
||||
+33
-15
@@ -80,8 +80,14 @@ const DELAY_UNITS = [
|
||||
];
|
||||
const MIN_DELAY_MINUTES = 10;
|
||||
const MAX_DELAY_MINUTES = 43200; // 30 days
|
||||
// Conversation-level delayed rules can filter on status plus immutable attributes (inbox).
|
||||
const DELAYED_CONVERSATION_ATTRS = ['status', 'inbox_id'];
|
||||
// The only valid delayed (wait) rule shapes: each supported event maps to the conditions that
|
||||
// stay meaningful across the wait — status / message_type re-checked at fire time, plus the
|
||||
// immutable inbox for scoping. Every other event/condition is hidden while the wait is on.
|
||||
const DELAYED_EVENT_ATTRS = {
|
||||
conversation_updated: ['status', 'inbox_id'],
|
||||
message_created: ['message_type', 'inbox_id'],
|
||||
};
|
||||
const DELAYED_EVENTS = Object.keys(DELAYED_EVENT_ATTRS);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isCloudFeatureEnabled } = useAccount();
|
||||
@@ -129,11 +135,10 @@ const filterTypes = computed(() => {
|
||||
if (!event || !props.automationTypes[event]) return [];
|
||||
|
||||
let attributes = getTranslatedAttributes(props.automationTypes, event);
|
||||
// A delayed conversation-level rule can filter only on status and immutable attributes (inbox).
|
||||
if (isDelayed.value && event !== 'message_created') {
|
||||
attributes = attributes.filter(attr =>
|
||||
DELAYED_CONVERSATION_ATTRS.includes(attr.key)
|
||||
);
|
||||
// A delayed rule can only filter on the attributes that stay meaningful across the wait.
|
||||
if (isDelayed.value) {
|
||||
const allowed = DELAYED_EVENT_ATTRS[event] || [];
|
||||
attributes = attributes.filter(attr => allowed.includes(attr.key));
|
||||
}
|
||||
|
||||
return attributes.map(attr => {
|
||||
@@ -174,12 +179,15 @@ const filterTypes = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const automationRuleEvents = computed(() =>
|
||||
AUTOMATION_RULE_EVENTS.map(event => ({
|
||||
const automationRuleEvents = computed(() => {
|
||||
const events = isDelayed.value
|
||||
? AUTOMATION_RULE_EVENTS.filter(event => DELAYED_EVENTS.includes(event.key))
|
||||
: AUTOMATION_RULE_EVENTS;
|
||||
return events.map(event => ({
|
||||
...event,
|
||||
value: t(`AUTOMATION.EVENTS.${event.value}`),
|
||||
}))
|
||||
);
|
||||
}));
|
||||
});
|
||||
|
||||
const hasAutomationMutated = computed(() => {
|
||||
return Boolean(
|
||||
@@ -221,15 +229,15 @@ const delayRestrictionReason = computed(() => {
|
||||
) {
|
||||
return 'ATTRIBUTE_CHANGED';
|
||||
}
|
||||
const allowed = DELAYED_EVENT_ATTRS[eventName.value];
|
||||
if (!allowed) return 'UNSUPPORTED_EVENT';
|
||||
if (
|
||||
eventName.value !== 'message_created' &&
|
||||
conditions.some(
|
||||
condition =>
|
||||
condition.attribute_key &&
|
||||
!DELAYED_CONVERSATION_ATTRS.includes(condition.attribute_key)
|
||||
condition.attribute_key && !allowed.includes(condition.attribute_key)
|
||||
)
|
||||
) {
|
||||
return 'CONVERSATION_NON_STATUS';
|
||||
return 'UNSUPPORTED_CONDITION';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -311,6 +319,16 @@ const resetToSupportedCondition = () => {
|
||||
];
|
||||
};
|
||||
|
||||
// A delay narrows the event list, so if the wait is turned on while an unsupported event is
|
||||
// selected (e.g. conversation_opened), switch to a meaningful default and reset its conditions
|
||||
// and actions the same way the event dropdown would.
|
||||
watch(isDelayed, delayed => {
|
||||
if (!delayed || !automation.value) return;
|
||||
if (DELAYED_EVENTS.includes(automation.value.event_name)) return;
|
||||
automation.value.event_name = DELAYED_EVENTS[0];
|
||||
props.onEventChange();
|
||||
});
|
||||
|
||||
// A delay narrows the condition options, so whenever the rule becomes unsupported while the
|
||||
// wait is on — toggling it on, or switching to an event whose default condition isn't allowed
|
||||
// (e.g. conversation_opened defaults to browser_language) — reset to a supported default.
|
||||
|
||||
@@ -35,6 +35,31 @@ const filteredRecords = computed(() => {
|
||||
if (!query) return records.value;
|
||||
return picoSearch(records.value, query, ['name', 'description']);
|
||||
});
|
||||
|
||||
// Delayed (wait) rules run on a different lifecycle, so list them in their own section.
|
||||
const hasDelayedRecords = computed(() =>
|
||||
records.value.some(automation => automation.execution_delay)
|
||||
);
|
||||
|
||||
const sections = computed(() => {
|
||||
const instant = [];
|
||||
const delayed = [];
|
||||
filteredRecords.value.forEach(automation =>
|
||||
(automation.execution_delay ? delayed : instant).push(automation)
|
||||
);
|
||||
return [
|
||||
{
|
||||
key: 'instant',
|
||||
label: t('AUTOMATION.LIST.SECTIONS.INSTANT'),
|
||||
items: instant,
|
||||
},
|
||||
{
|
||||
key: 'delayed',
|
||||
label: t('AUTOMATION.LIST.SECTIONS.DELAYED'),
|
||||
items: delayed,
|
||||
},
|
||||
].filter(section => section.items.length);
|
||||
});
|
||||
const uiFlags = computed(() => getters['automations/getUIFlags'].value);
|
||||
const accountId = computed(() => getters.getCurrentAccountId.value);
|
||||
|
||||
@@ -226,25 +251,43 @@ const tableHeaders = computed(() => {
|
||||
>
|
||||
{{ $t('AUTOMATION.LIST.DELAY_DISABLED_BANNER') }}
|
||||
</div>
|
||||
<template v-if="filteredRecords.length">
|
||||
<div
|
||||
v-for="section in sections"
|
||||
:key="section.key"
|
||||
class="mb-6 last:mb-0"
|
||||
>
|
||||
<h4
|
||||
v-if="hasDelayedRecords"
|
||||
class="mb-2 text-sm font-medium text-n-slate-11"
|
||||
>
|
||||
{{ section.label }}
|
||||
</h4>
|
||||
<BaseTable :headers="tableHeaders" :items="section.items">
|
||||
<template #row="{ items }">
|
||||
<AutomationRuleRow
|
||||
v-for="automation in items"
|
||||
:key="automation.id"
|
||||
:automation="automation"
|
||||
:loading="loading[automation.id]"
|
||||
@clone="cloneAutomation"
|
||||
@toggle="toggleAutomation"
|
||||
@edit="openEditPopup"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</template>
|
||||
<BaseTable
|
||||
v-else
|
||||
:headers="tableHeaders"
|
||||
:items="filteredRecords"
|
||||
:items="[]"
|
||||
:no-data-message="
|
||||
searchQuery ? $t('AUTOMATION.NO_RESULTS') : $t('AUTOMATION.LIST.404')
|
||||
"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<AutomationRuleRow
|
||||
v-for="automation in items"
|
||||
:key="automation.id"
|
||||
:automation="automation"
|
||||
:loading="loading[automation.id]"
|
||||
@clone="cloneAutomation"
|
||||
@toggle="toggleAutomation"
|
||||
@edit="openEditPopup"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</template>
|
||||
<template #row />
|
||||
</BaseTable>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -23,9 +23,13 @@ class AutomationRule < ApplicationRecord
|
||||
include Reauthorizable
|
||||
|
||||
EXECUTION_DELAY_RANGE = (10..43_200) # minutes: 10 min to 30 days
|
||||
# Conversation-level delayed rules key their episode on status; only status and attributes
|
||||
# that never change after the delay (inbox) are safe to also filter on.
|
||||
DELAYED_CONVERSATION_ATTRIBUTES = %w[status inbox_id].freeze
|
||||
# The only valid delayed (wait) rule shapes: each supported event maps to the conditions that
|
||||
# stay meaningful across the wait — status / message_type re-checked at fire time, plus the
|
||||
# immutable inbox for scoping. Every other event or condition is rejected for delayed rules.
|
||||
DELAYED_EVENT_ATTRIBUTES = {
|
||||
'conversation_updated' => %w[status inbox_id],
|
||||
'message_created' => %w[message_type inbox_id]
|
||||
}.freeze
|
||||
|
||||
belongs_to :account
|
||||
has_many :pending_executions, class_name: 'AutomationRulePendingExecution', dependent: :delete_all
|
||||
@@ -116,13 +120,16 @@ class AutomationRule < ApplicationRecord
|
||||
errors.add(:execution_delay, 'cannot be used with attribute_changed conditions.')
|
||||
end
|
||||
|
||||
# Conversation-level episodes key on status_changed_at alone. Mutable attributes would collapse
|
||||
# distinct periods into one episode, so only status and immutable filters (inbox) are allowed.
|
||||
# Delayed rules are limited to the shapes whose episode keys stay meaningful across the wait:
|
||||
# conversation_updated on status/inbox, and message_created on message_type/inbox.
|
||||
def execution_delay_supported_event
|
||||
return if execution_delay.blank? || conditions.blank? || event_name == 'message_created'
|
||||
return if conditions.all? { |obj| DELAYED_CONVERSATION_ATTRIBUTES.include?(obj['attribute_key']) }
|
||||
return if execution_delay.blank?
|
||||
|
||||
errors.add(:execution_delay, 'only supports status and inbox conditions for conversation-level events.')
|
||||
allowed = DELAYED_EVENT_ATTRIBUTES[event_name]
|
||||
return errors.add(:execution_delay, 'is only supported for conversation_updated and message_created events.') if allowed.nil?
|
||||
return if conditions.blank? || conditions.all? { |obj| allowed.include?(obj['attribute_key']) }
|
||||
|
||||
errors.add(:execution_delay, "only supports #{allowed.join(' and ')} conditions for #{event_name} events.")
|
||||
end
|
||||
|
||||
def execution_config_changed?
|
||||
|
||||
Reference in New Issue
Block a user