mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-23 19:06:35 +00:00
rework notify action with subject, message and picked recipients
The notify action only created a fixed in-app notification and recipients
were typed as a raw team:<id> / user:<id> DSL where typos silently notified
nobody. Now:
- recipients are picked by name (assignee, assigned team, any team or agent)
- admin writes the subject and message; both show in the bell notification
and go out as an email (message plus conversation link, wrapped in the
default outgoing email template, so nothing is hardcoded in English)
- the action serializes as typed fields {subject, message, recipients}
instead of a positional value array
Also: previous_* condition fields are hidden for time triggers and populated
on message events (previous = current) so those rules can actually match,
action row widths now follow the RuleBox pattern (fixed type select, value
widgets fill the row), and three new i18n keys merged into existing globals.
This commit is contained in:
@@ -339,7 +339,7 @@ export function useConversationFilters () {
|
||||
type: FIELD_TYPE.RECIPIENTS
|
||||
},
|
||||
snooze: {
|
||||
label: t('actions.snooze'),
|
||||
label: t('globals.terms.snooze'),
|
||||
type: FIELD_TYPE.TEXT
|
||||
},
|
||||
trigger_webhook: {
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex gap-5">
|
||||
<div class="w-48">
|
||||
<div class="flex items-start justify-between gap-5">
|
||||
<div class="flex gap-5 flex-1 min-w-0">
|
||||
<div class="w-56 shrink-0">
|
||||
<!-- Type -->
|
||||
<Select
|
||||
v-model="action.type"
|
||||
@@ -35,7 +35,7 @@
|
||||
<!-- Value -->
|
||||
<div
|
||||
v-if="action.type && conversationActions[action.type]?.type === 'tag'"
|
||||
class="w-full"
|
||||
class="flex-1 min-w-0"
|
||||
>
|
||||
<SelectTag
|
||||
v-model="action.value"
|
||||
@@ -46,32 +46,29 @@
|
||||
|
||||
<div
|
||||
v-if="action.type && conversationActions[action.type]?.type === 'recipients'"
|
||||
class="w-full"
|
||||
class="flex-1 min-w-0 space-y-3"
|
||||
>
|
||||
<TagsInput
|
||||
:modelValue="action.value || []"
|
||||
@update:modelValue="(value) => handleNotifyChange(value, index)"
|
||||
:addOnBlur="true"
|
||||
:addOnTab="true"
|
||||
:addOnPaste="true"
|
||||
>
|
||||
<TagsInputItem
|
||||
v-for="item in action.value || []"
|
||||
:key="item"
|
||||
:value="item"
|
||||
>
|
||||
<TagsInputItemText />
|
||||
<TagsInputItemDelete />
|
||||
</TagsInputItem>
|
||||
<TagsInputInput :placeholder="t('placeholders.notifyRecipient')" />
|
||||
</TagsInput>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
{{ $t('admin.automation.notifyRecipientHint') }}
|
||||
</p>
|
||||
<SelectTag
|
||||
:modelValue="action.recipients || []"
|
||||
@update:modelValue="(value) => handleNotifyRecipientsChange(value, index)"
|
||||
:items="notifyRecipientOptions"
|
||||
:placeholder="t('placeholders.selectRecipients')"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="t('globals.terms.subject')"
|
||||
:modelValue="action.subject || ''"
|
||||
@update:modelValue="(value) => handleNotifySubjectChange(value, index)"
|
||||
/>
|
||||
<Textarea
|
||||
:placeholder="t('globals.terms.message')"
|
||||
:modelValue="action.message || ''"
|
||||
@update:modelValue="(value) => handleNotifyMessageChange(value, index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-48"
|
||||
class="flex-1 min-w-0"
|
||||
v-if="action.type && conversationActions[action.type]?.type === 'select'"
|
||||
>
|
||||
<SelectComboBox
|
||||
@@ -84,10 +81,10 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex gap-3"
|
||||
class="flex gap-3 flex-1 min-w-0"
|
||||
v-if="action.type && conversationActions[action.type]?.type === 'webhook'"
|
||||
>
|
||||
<div class="w-48">
|
||||
<div class="flex-1 min-w-0">
|
||||
<SelectComboBox
|
||||
v-model="action.value[0]"
|
||||
:items="webhookStore.options"
|
||||
@@ -95,7 +92,7 @@
|
||||
@select="handleWebhookChange($event, index)"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<div class="flex-1 min-w-0">
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="t('placeholders.webhookEventName')"
|
||||
@@ -109,7 +106,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-48"
|
||||
class="flex-1 min-w-0"
|
||||
v-if="action.type && conversationActions[action.type]?.type === 'text'"
|
||||
>
|
||||
<Input
|
||||
@@ -147,12 +144,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { toRefs } from 'vue'
|
||||
import { toRefs, computed } from 'vue'
|
||||
import { Button } from '@shared-ui/components/ui/button'
|
||||
import { Input } from '@shared-ui/components/ui/input'
|
||||
import { Textarea } from '@shared-ui/components/ui/textarea'
|
||||
import CloseButton from '@main/components/button/CloseButton.vue'
|
||||
import { useTagStore } from '@main/stores/tag'
|
||||
import { useWebhookStore } from '@main/stores/webhook'
|
||||
import { useUsersStore } from '@main/stores/users'
|
||||
import { useTeamStore } from '@main/stores/team'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -162,13 +162,6 @@ import {
|
||||
SelectValue
|
||||
} from '@shared-ui/components/ui/select'
|
||||
import { SelectTag } from '@shared-ui/components/ui/select'
|
||||
import {
|
||||
TagsInput,
|
||||
TagsInputInput,
|
||||
TagsInputItem,
|
||||
TagsInputItemDelete,
|
||||
TagsInputItemText
|
||||
} from '@shared-ui/components/ui/tags-input'
|
||||
import { useConversationFilters } from '@main/composables/useConversationFilters'
|
||||
import { getTextFromHTML } from '@shared-ui/utils/string'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -187,12 +180,37 @@ const { t } = useI18n()
|
||||
const emit = defineEmits(['update-actions', 'add-action', 'remove-action'])
|
||||
const tagsStore = useTagStore()
|
||||
const webhookStore = useWebhookStore()
|
||||
const uStore = useUsersStore()
|
||||
const tStore = useTeamStore()
|
||||
const { conversationActions } = useConversationFilters()
|
||||
|
||||
webhookStore.fetchWebhooks()
|
||||
|
||||
const handleNotifyChange = (value, index) => {
|
||||
actions.value[index].value = value || []
|
||||
const notifyRecipientOptions = computed(() => [
|
||||
{ label: t('admin.automation.notifyAssignee'), value: 'assignee' },
|
||||
{ label: t('admin.automation.notifyAssignedTeam'), value: 'assigned_team' },
|
||||
...tStore.options.map((o) => ({
|
||||
label: `${t('globals.terms.team')}: ${o.label}`,
|
||||
value: `team:${o.value}`
|
||||
})),
|
||||
...uStore.options.map((o) => ({
|
||||
label: `${t('globals.terms.agent')}: ${o.label}`,
|
||||
value: `user:${o.value}`
|
||||
}))
|
||||
])
|
||||
|
||||
const handleNotifyRecipientsChange = (value, index) => {
|
||||
actions.value[index].recipients = value || []
|
||||
emitUpdate(index)
|
||||
}
|
||||
|
||||
const handleNotifySubjectChange = (value, index) => {
|
||||
actions.value[index].subject = value
|
||||
emitUpdate(index)
|
||||
}
|
||||
|
||||
const handleNotifyMessageChange = (value, index) => {
|
||||
actions.value[index].message = value
|
||||
emitUpdate(index)
|
||||
}
|
||||
|
||||
@@ -222,8 +240,12 @@ const placeholderForText = (type) => {
|
||||
}
|
||||
|
||||
const handleFieldChange = (value, index) => {
|
||||
actions.value[index].value = []
|
||||
actions.value[index].type = value
|
||||
const action = actions.value[index]
|
||||
action.value = []
|
||||
action.type = value
|
||||
delete action.subject
|
||||
delete action.message
|
||||
delete action.recipients
|
||||
emitUpdate(index)
|
||||
}
|
||||
|
||||
|
||||
@@ -250,9 +250,14 @@ const { t } = useI18n()
|
||||
|
||||
// Computed property to get the correct filters based on type
|
||||
const currentFilters = computed(() => {
|
||||
return props.type === 'new_conversation'
|
||||
? newConversationFilters.value
|
||||
: conversationFilters.value
|
||||
if (props.type === 'new_conversation') return newConversationFilters.value
|
||||
if (props.type === 'conversation_update') return conversationFilters.value
|
||||
// previous_* values only exist on conversation update events.
|
||||
const filters = { ...conversationFilters.value }
|
||||
for (const key of Object.keys(filters)) {
|
||||
if (key.startsWith('previous_')) delete filters[key]
|
||||
}
|
||||
return filters
|
||||
})
|
||||
|
||||
// Watch for type change and reset the rules as the fields will change
|
||||
|
||||
@@ -404,6 +404,14 @@ const getRulesValidationError = () => {
|
||||
action.value = ['0']
|
||||
}
|
||||
|
||||
// Notify uses its own fields instead of value.
|
||||
if (action.type === 'notify') {
|
||||
if (!action.subject?.trim() || !action.message?.trim() || !action.recipients?.length) {
|
||||
return t('admin.automation.validation.setActionValue')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Empty array, no value selected.
|
||||
if (action.value.length === 0) {
|
||||
return t('admin.automation.validation.setActionValue')
|
||||
|
||||
+4
-5
@@ -12,7 +12,7 @@
|
||||
"actions.addingPrivateNotes": "Adding private notes",
|
||||
"actions.addPrivateNote": "Add private note",
|
||||
"actions.applyMacro": "Apply macro",
|
||||
"actions.notify": "Notify",
|
||||
"actions.notify": "Notify agents",
|
||||
"actions.openMacros": "Open macros",
|
||||
"actions.assignAgent": "Assign agent",
|
||||
"actions.assignTeam": "Assign team",
|
||||
@@ -26,7 +26,6 @@
|
||||
"actions.setStatus": "Set status",
|
||||
"actions.setTags": "Set tags",
|
||||
"actions.setValue": "Set value",
|
||||
"actions.snooze": "Snooze",
|
||||
"actions.startingConversation": "Starting conversation",
|
||||
"actions.triggerWebhook": "Trigger webhook",
|
||||
"activityLog.agentAway": "{actorEmail} ({actorId}) changed {targetEmail} ({targetId}) status to away",
|
||||
@@ -72,7 +71,8 @@
|
||||
"admin.automation.matchTheseRules": "Match these rules",
|
||||
"admin.automation.newConversation.description": "Rules that run when a new conversation is created by a contact. Conversations initiated by agents do not trigger these rules. Drag and drop to reorder.",
|
||||
"admin.automation.noRulesFound": "No rules found",
|
||||
"admin.automation.notifyRecipientHint": "Enter recipients as assignee, assigned_team, team:<id>, or user:<id>. Press Enter after each.",
|
||||
"admin.automation.notifyAssignedTeam": "Assigned team",
|
||||
"admin.automation.notifyAssignee": "Assignee",
|
||||
"admin.automation.webhookEventNameHint": "Event name sent in the webhook payload.",
|
||||
"admin.automation.or": "OR",
|
||||
"admin.automation.performTheseActions": "Perform these actions",
|
||||
@@ -1130,7 +1130,6 @@
|
||||
"navigation.keyboardShortcuts": "Keyboard shortcuts",
|
||||
"navigation.logout": "Logout",
|
||||
"navigation.reassignReplies": "Reassign replies",
|
||||
"notification.automationNotify": "Automation notification for #{referenceNumber}",
|
||||
"notification.conversationAssigned": "Conversation assigned to you #{referenceNumber}",
|
||||
"notification.mentionedInConversation": "{author} mentioned you in #{referenceNumber}",
|
||||
"notification.slaAlert": "SLA {type}: {metric} for #{referenceNumber}",
|
||||
@@ -1146,7 +1145,6 @@
|
||||
"placeholders.introductionMessage": "We're here to help!",
|
||||
"placeholders.linkText": "Link Text",
|
||||
"placeholders.noticeBannerText": "Our response times are slower than usual. We're working hard to get to your message.",
|
||||
"placeholders.notifyRecipient": "assignee, team:5, user:42",
|
||||
"placeholders.selectAction": "Select action",
|
||||
"placeholders.selectAgent": "Select agent",
|
||||
"placeholders.selectCsvFile": "Select CSV file",
|
||||
@@ -1158,6 +1156,7 @@
|
||||
"placeholders.selectPriority": "Select priority",
|
||||
"placeholders.selectProtocol": "Select protocol",
|
||||
"placeholders.selectProvider": "Select provider",
|
||||
"placeholders.selectRecipients": "Select recipients",
|
||||
"placeholders.selectTags": "Select tags",
|
||||
"placeholders.selectTeam": "Select team",
|
||||
"placeholders.selectTeams": "Select teams",
|
||||
|
||||
@@ -330,7 +330,7 @@ func (e *Engine) EvaluateConversationUpdateRulesByID(conversationID int, convers
|
||||
e.lo.Error("error fetching conversation", "conversation_id", conversationID, "error", err)
|
||||
return
|
||||
}
|
||||
e.EvaluateConversationUpdateRules(conversation, eventType, nil)
|
||||
e.EvaluateConversationUpdateRules(conversation, eventType, models.PreviousValues(conversation))
|
||||
}
|
||||
|
||||
// handleNewConversation handles new conversation events.
|
||||
|
||||
@@ -1480,7 +1480,7 @@ func TestStartsWithOperator(t *testing.T) {
|
||||
|
||||
func TestNotifyAction_PassedThrough(t *testing.T) {
|
||||
mockStore := new(mockConversationStore)
|
||||
expected := models.RuleAction{Type: models.ActionNotify, Value: []string{"assignee", "team:5"}}
|
||||
expected := models.RuleAction{Type: models.ActionNotify, Subject: "subject", Message: "message", Recipients: []string{"assignee", "team:5"}}
|
||||
mockStore.On("ApplyAction", expected, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
engine := createTestEngine(mockStore)
|
||||
|
||||
@@ -1526,8 +1526,8 @@ func TestMultipleNotifyActions(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
[]models.RuleAction{
|
||||
{Type: models.ActionNotify, Value: []string{"assignee"}},
|
||||
{Type: models.ActionNotify, Value: []string{"team:5"}},
|
||||
{Type: models.ActionNotify, Subject: "subject", Message: "message", Recipients: []string{"assignee"}},
|
||||
{Type: models.ActionNotify, Subject: "subject", Message: "message", Recipients: []string{"team:5"}},
|
||||
},
|
||||
models.OperatorOR,
|
||||
),
|
||||
|
||||
@@ -2,9 +2,11 @@ package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
authzModels "github.com/abhinavxd/libredesk/internal/authz/models"
|
||||
cmodels "github.com/abhinavxd/libredesk/internal/conversation/models"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
@@ -135,4 +137,32 @@ type RuleAction struct {
|
||||
Type string `json:"type" db:"type"`
|
||||
Value []string `json:"value" db:"value"`
|
||||
DisplayValue []string `json:"display_value" db:"-"`
|
||||
|
||||
// Set only for the notify action.
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Recipients []string `json:"recipients,omitempty"`
|
||||
}
|
||||
|
||||
// PreviousValues returns conv's field values keyed for previous_* filters; pass the pre-change conversation.
|
||||
func PreviousValues(conv cmodels.Conversation) map[string]string {
|
||||
values := map[string]string{
|
||||
ConversationPreviousStatus: "",
|
||||
ConversationPreviousPriority: "",
|
||||
ConversationPreviousAssignedUser: "",
|
||||
ConversationPreviousAssignedTeam: "",
|
||||
}
|
||||
if conv.StatusID.Valid {
|
||||
values[ConversationPreviousStatus] = strconv.Itoa(conv.StatusID.Int)
|
||||
}
|
||||
if conv.PriorityID.Valid {
|
||||
values[ConversationPreviousPriority] = strconv.Itoa(conv.PriorityID.Int)
|
||||
}
|
||||
if conv.AssignedUserID.Valid {
|
||||
values[ConversationPreviousAssignedUser] = strconv.Itoa(conv.AssignedUserID.Int)
|
||||
}
|
||||
if conv.AssignedTeamID.Valid {
|
||||
values[ConversationPreviousAssignedTeam] = strconv.Itoa(conv.AssignedTeamID.Int)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ var (
|
||||
|
||||
const (
|
||||
conversationsListMaxPageSize = 500
|
||||
|
||||
automationNotifyEmailContent = `<p style="white-space: pre-line;">{{ .Message }}</p>
|
||||
|
||||
<p>
|
||||
<a href="{{ RootURL }}/inboxes/all/conversation/{{ .Conversation.UUID }}">#{{ .Conversation.ReferenceNumber }}</a>
|
||||
</p>`
|
||||
)
|
||||
|
||||
var conversationFilterRenderers = dbutil.FieldRenderers{
|
||||
@@ -746,7 +752,7 @@ func (c *Manager) UpdateConversationUserAssignee(uuid string, assigneeID int, ac
|
||||
if err := c.updateAssignee(uuid, assigneeID, models.AssigneeTypeUser); err != nil {
|
||||
return envelope.NewError(envelope.GeneralError, c.i18n.T("globals.messages.somethingWentWrong"), nil)
|
||||
}
|
||||
return c.afterUserAssignedHooks(uuid, assigneeID, actor, previousValuesFor(previousConversation))
|
||||
return c.afterUserAssignedHooks(uuid, assigneeID, actor, amodels.PreviousValues(previousConversation))
|
||||
}
|
||||
|
||||
// ClaimUnassignedConversation atomically assigns a conversation only if still unassigned and still in expectedTeamID, else returns ErrConversationAlreadyAssigned.
|
||||
@@ -834,7 +840,7 @@ func (c *Manager) UpdateConversationTeamAssignee(uuid string, teamID int, actor
|
||||
return err
|
||||
}
|
||||
previousAssignedTeamID := conversation.AssignedTeamID.Int
|
||||
previousValues := previousValuesFor(conversation)
|
||||
previousValues := amodels.PreviousValues(conversation)
|
||||
|
||||
if err := c.updateAssignee(uuid, teamID, models.AssigneeTypeTeam); err != nil {
|
||||
return envelope.NewError(envelope.GeneralError, c.i18n.T("globals.messages.somethingWentWrong"), nil)
|
||||
@@ -923,7 +929,7 @@ func (c *Manager) UpdateConversationPriority(uuid string, priorityID int, priori
|
||||
|
||||
conversation, err := c.GetConversation(0, uuid, "")
|
||||
if err == nil {
|
||||
c.automation.EvaluateConversationUpdateRules(conversation, amodels.EventConversationPriorityChange, previousValuesFor(previousConversation))
|
||||
c.automation.EvaluateConversationUpdateRules(conversation, amodels.EventConversationPriorityChange, amodels.PreviousValues(previousConversation))
|
||||
}
|
||||
|
||||
// Record activity.
|
||||
@@ -1040,7 +1046,7 @@ func (c *Manager) UpdateConversationStatus(uuid string, statusID int, status, sn
|
||||
c.BroadcastConversationUpdate(uuid, agentData)
|
||||
|
||||
if conversation.ID != 0 {
|
||||
c.automation.EvaluateConversationUpdateRules(conversation, amodels.EventConversationStatusChange, previousValuesFor(conversationBeforeChange))
|
||||
c.automation.EvaluateConversationUpdateRules(conversation, amodels.EventConversationStatusChange, amodels.PreviousValues(conversationBeforeChange))
|
||||
}
|
||||
|
||||
// Broadcast conversation update to widget clients.
|
||||
@@ -1397,8 +1403,8 @@ func (m *Manager) ApplySLA(conversation models.Conversation, policyID int, actor
|
||||
// ApplyAction applies an action to a conversation, this can be called from multiple packages across the app to perform actions on conversations.
|
||||
// all actions are executed on behalf of the provided user if the user is not provided, system user is used.
|
||||
func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversation, user umodels.User) error {
|
||||
// CSAT action does not require a value.
|
||||
if len(action.Value) == 0 && action.Type != amodels.ActionSendCSAT {
|
||||
// CSAT has no value; notify carries its data in dedicated fields.
|
||||
if len(action.Value) == 0 && action.Type != amodels.ActionSendCSAT && action.Type != amodels.ActionNotify {
|
||||
return fmt.Errorf("empty value for action %s", action.Type)
|
||||
}
|
||||
|
||||
@@ -1504,14 +1510,19 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversatio
|
||||
})
|
||||
return nil
|
||||
case amodels.ActionNotify:
|
||||
return m.notifyAutomation(action.Value, conv)
|
||||
subject := strings.TrimSpace(action.Subject)
|
||||
message := strings.TrimSpace(action.Message)
|
||||
if subject == "" || message == "" || len(action.Recipients) == 0 {
|
||||
return fmt.Errorf("notify action requires a subject, a message and at least one recipient")
|
||||
}
|
||||
return m.notifyAutomation(subject, message, action.Recipients, conv)
|
||||
default:
|
||||
return fmt.Errorf("unknown action: %s", action.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) notifyAutomation(entries []string, conv models.Conversation) error {
|
||||
func (m *Manager) notifyAutomation(subject, message string, entries []string, conv models.Conversation) error {
|
||||
userIDs := m.resolveNotifyRecipients(entries, conv)
|
||||
if len(userIDs) == 0 {
|
||||
m.lo.Debug("notify action: no recipients resolved", "conversation_uuid", conv.UUID)
|
||||
@@ -1521,14 +1532,45 @@ func (m *Manager) notifyAutomation(entries []string, conv models.Conversation) e
|
||||
m.lo.Warn("notify action: recipient cap reached, truncating", "original", len(userIDs), "cap", amodels.MaxNotifyRecipients, "conversation_uuid", conv.UUID)
|
||||
userIDs = userIDs[:amodels.MaxNotifyRecipients]
|
||||
}
|
||||
m.dispatcher.Send(notifier.Notification{
|
||||
|
||||
notification := notifier.Notification{
|
||||
Type: nmodels.NotificationTypeMention,
|
||||
RecipientIDs: userIDs,
|
||||
Title: m.i18n.Ts("notification.automationNotify", "referenceNumber", conv.ReferenceNumber),
|
||||
Body: conv.Subject,
|
||||
Title: subject,
|
||||
Body: null.StringFrom(message),
|
||||
ConversationID: null.IntFrom(conv.ID),
|
||||
ConversationUUID: conv.UUID,
|
||||
})
|
||||
}
|
||||
|
||||
content, err := m.template.RenderEmailWithTemplate(
|
||||
map[string]any{
|
||||
"Conversation": map[string]any{
|
||||
"ReferenceNumber": conv.ReferenceNumber,
|
||||
"UUID": conv.UUID,
|
||||
},
|
||||
"Message": message,
|
||||
},
|
||||
automationNotifyEmailContent)
|
||||
if err != nil {
|
||||
m.lo.Error("error rendering automation notify email", "conversation_uuid", conv.UUID, "error", err)
|
||||
} else {
|
||||
emails := make([]string, len(userIDs))
|
||||
for i, id := range userIDs {
|
||||
agent, err := m.userStore.GetAgent(id, "")
|
||||
if err != nil {
|
||||
m.lo.Error("notify: error fetching agent for email", "user_id", id, "error", err)
|
||||
continue
|
||||
}
|
||||
emails[i] = agent.Email.String
|
||||
}
|
||||
notification.Email = ¬ifier.EmailNotification{
|
||||
Recipients: emails,
|
||||
Subject: notification.Title,
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
|
||||
m.dispatcher.Send(notification)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2144,29 +2186,6 @@ func parseNotifyRecipient(entry string) (string, int) {
|
||||
return kind, id
|
||||
}
|
||||
|
||||
// previousValuesFor returns the pre-change field values consumed by previous_* automation filters.
|
||||
func previousValuesFor(conv models.Conversation) map[string]string {
|
||||
values := map[string]string{
|
||||
amodels.ConversationPreviousStatus: "",
|
||||
amodels.ConversationPreviousPriority: "",
|
||||
amodels.ConversationPreviousAssignedUser: "",
|
||||
amodels.ConversationPreviousAssignedTeam: "",
|
||||
}
|
||||
if conv.StatusID.Valid {
|
||||
values[amodels.ConversationPreviousStatus] = strconv.Itoa(conv.StatusID.Int)
|
||||
}
|
||||
if conv.PriorityID.Valid {
|
||||
values[amodels.ConversationPreviousPriority] = strconv.Itoa(conv.PriorityID.Int)
|
||||
}
|
||||
if conv.AssignedUserID.Valid {
|
||||
values[amodels.ConversationPreviousAssignedUser] = strconv.Itoa(conv.AssignedUserID.Int)
|
||||
}
|
||||
if conv.AssignedTeamID.Valid {
|
||||
values[amodels.ConversationPreviousAssignedTeam] = strconv.Itoa(conv.AssignedTeamID.Int)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func nullTimeOrNil(t null.Time) any {
|
||||
if !t.Valid || t.Time.IsZero() {
|
||||
return nil
|
||||
|
||||
@@ -1393,7 +1393,7 @@ func (m *Manager) ProcessIncomingMessageHooks(conversationUUID string, isNewConv
|
||||
m.lo.Error("error fetching conversation for incoming message hooks", "conversation_uuid", conversationUUID, "error", err)
|
||||
} else {
|
||||
// Trigger automations on incoming message event.
|
||||
m.automation.EvaluateConversationUpdateRules(conversation, amodels.EventConversationMessageIncoming, nil)
|
||||
m.automation.EvaluateConversationUpdateRules(conversation, amodels.EventConversationMessageIncoming, amodels.PreviousValues(conversation))
|
||||
|
||||
// If assigned to an AI assistant, let it respond to this inbound customer message.
|
||||
if m.aiAgent != nil && conversation.AssignedUserID.Valid {
|
||||
|
||||
Reference in New Issue
Block a user