Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
g.DELETE("/api/v1/roles/{id}", perm(handleDeleteRole, "roles:manage"))

// Webhooks.
g.GET("/api/v1/webhooks/compact", auth(handleGetWebhooksCompact))
g.GET("/api/v1/webhooks", perm(handleGetWebhooks, "webhooks:manage"))
g.GET("/api/v1/webhooks/{id}", perm(handleGetWebhook, "webhooks:manage"))
g.POST("/api/v1/webhooks", perm(handleCreateWebhook, "webhooks:manage"))
Expand Down
5 changes: 5 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ func main() {

wsHub.SetConversationStore(conversation)
automation.SetConversationStore(conversation)
systemUser, err := user.GetSystemUser()
if err != nil {
log.Fatalf("error fetching system user: %v", err)
}
automation.SetSystemUserID(systemUser.ID)
conversation.SetAIAgent(aiAgent)

startInboxes(ctx, inbox, conversation, user, conversation.SignAvatarURL)
Expand Down
12 changes: 12 additions & 0 deletions cmd/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ import (
"github.com/zerodha/fastglue"
)

// handleGetWebhooksCompact returns all webhooks in a compact format.
func handleGetWebhooksCompact(r *fastglue.Request) error {
var (
app = r.Context.(*App)
)
webhooks, err := app.webhook.GetAllCompact()
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(webhooks)
}

// handleGetWebhooks returns all webhooks from the database.
func handleGetWebhooks(r *fastglue.Request) error {
var (
Expand Down
2 changes: 2 additions & 0 deletions frontend/apps/main/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ const createContactNote = (id, data) => http.post(`/api/v1/contacts/${id}/notes`
})
const deleteContactNote = (id, noteId) => http.delete(`/api/v1/contacts/${id}/notes/${noteId}`)
const getActivityLogs = (params) => http.get('/api/v1/activity-logs', { params })
const getWebhooksCompact = () => http.get('/api/v1/webhooks/compact')
const getWebhooks = () => http.get('/api/v1/webhooks')
const getWebhook = (id) => http.get(`/api/v1/webhooks/${id}`)
const createWebhook = (data) =>
Expand Down Expand Up @@ -744,6 +745,7 @@ export default {
createContactNote,
deleteContactNote,
getActivityLogs,
getWebhooksCompact,
getWebhooks,
getWebhook,
createWebhook,
Expand Down
46 changes: 41 additions & 5 deletions frontend/apps/main/src/composables/useConversationFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ export function useConversationFilters () {
}

const customAttributeDataTypeToFieldOperators = {
'text': FIELD_OPERATORS.TEXT,
'text': FIELD_OPERATORS.TEXT_AUTOMATION,
'number': FIELD_OPERATORS.NUMBER,
'checkbox': FIELD_OPERATORS.BOOLEAN,
'date': FIELD_OPERATORS.DATE,
'link': FIELD_OPERATORS.TEXT,
'link': FIELD_OPERATORS.TEXT_AUTOMATION,
'list': FIELD_OPERATORS.SELECT,
}

Expand Down Expand Up @@ -164,17 +164,17 @@ export function useConversationFilters () {
contact_email: {
label: t('globals.terms.email'),
type: FIELD_TYPE.TEXT,
operators: FIELD_OPERATORS.TEXT
operators: FIELD_OPERATORS.TEXT_AUTOMATION
},
content: {
label: t('globals.terms.content'),
type: FIELD_TYPE.TEXT,
operators: FIELD_OPERATORS.TEXT
operators: FIELD_OPERATORS.TEXT_AUTOMATION
},
subject: {
label: t('globals.terms.subject'),
type: FIELD_TYPE.TEXT,
operators: FIELD_OPERATORS.TEXT
operators: FIELD_OPERATORS.TEXT_AUTOMATION
},
status: {
label: t('globals.terms.status'),
Expand Down Expand Up @@ -258,6 +258,30 @@ export function useConversationFilters () {
type: FIELD_TYPE.SELECT,
operators: FIELD_OPERATORS.SELECT,
options: iStore.options
},
previous_status: {
label: t('admin.automation.previousStatus'),
type: FIELD_TYPE.SELECT,
operators: FIELD_OPERATORS.SELECT,
options: cStore.statusOptions
},
previous_priority: {
label: t('admin.automation.previousPriority'),
type: FIELD_TYPE.SELECT,
operators: FIELD_OPERATORS.SELECT,
options: cStore.priorityOptions
},
previous_assigned_user: {
label: t('admin.automation.previousAssignedUser'),
type: FIELD_TYPE.SELECT,
operators: FIELD_OPERATORS.SELECT,
options: uStore.options
},
previous_assigned_team: {
label: t('admin.automation.previousAssignedTeam'),
type: FIELD_TYPE.SELECT,
operators: FIELD_OPERATORS.SELECT,
options: tStore.options
}
}))

Expand Down Expand Up @@ -309,6 +333,18 @@ export function useConversationFilters () {
remove_tags: {
label: t('actions.removeTags'),
type: FIELD_TYPE.TAG
},
notify: {
label: t('actions.notify'),
type: FIELD_TYPE.RECIPIENTS
},
snooze: {
label: t('globals.terms.snooze'),
type: FIELD_TYPE.TEXT
},
trigger_webhook: {
label: t('actions.triggerWebhook'),
type: FIELD_TYPE.WEBHOOK
}
}))

Expand Down
15 changes: 14 additions & 1 deletion frontend/apps/main/src/constants/filterConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const FIELD_TYPE = {
RICHTEXT: 'richtext',
BOOLEAN: 'boolean',
DATE: 'date',
WEBHOOK: 'webhook',
RECIPIENTS: 'recipients',
}

export const OPERATOR = {
Expand All @@ -26,7 +28,8 @@ export const OPERATOR = {
NOT_CONTAINS: 'not contains',
GREATER_THAN: 'greater than',
LESS_THAN: 'less than',
BETWEEN: 'between'
BETWEEN: 'between',
STARTS_WITH: 'starts with'
}

// operatorLabel returns a clearer display label for operators whose meaning is ambiguous with
Expand All @@ -51,6 +54,16 @@ export const FIELD_OPERATORS = {
OPERATOR.CONTAINS,
OPERATOR.NOT_CONTAINS
],
// "starts with" is only implemented by the automation evaluator, not the SQL filter builder.
TEXT_AUTOMATION: [
OPERATOR.EQUALS,
OPERATOR.NOT_EQUALS,
OPERATOR.SET,
OPERATOR.NOT_SET,
OPERATOR.CONTAINS,
OPERATOR.NOT_CONTAINS,
OPERATOR.STARTS_WITH
],
// For text columns that do not support partial matching, only allow exact match operators.
TEXT_EXACT: [OPERATOR.EQUALS, OPERATOR.NOT_EQUALS, OPERATOR.SET, OPERATOR.NOT_SET],
DATE: [
Expand Down
148 changes: 138 additions & 10 deletions frontend/apps/main/src/features/admin/automation/ActionBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -45,7 +45,31 @@
</div>

<div
class="w-48"
v-if="action.type && conversationActions[action.type]?.type === 'recipients'"
class="flex-1 min-w-0 space-y-3"
>
<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)"
class="h-36"
/>
</div>

<div
class="flex-1 min-w-0"
v-if="action.type && conversationActions[action.type]?.type === 'select'"
>
<SelectComboBox
Expand All @@ -56,6 +80,43 @@
:type="action.type === 'assign_team' ? 'team' : 'user'"
/>
</div>

<div
class="flex gap-3 flex-1 min-w-0"
v-if="action.type && conversationActions[action.type]?.type === 'webhook'"
>
<div class="flex-1 min-w-0">
<SelectComboBox
v-model="action.value[0]"
:items="webhookStore.options"
:placeholder="t('placeholders.selectWebhook')"
@select="handleWebhookChange($event, index)"
/>
</div>
<div class="flex-1 min-w-0">
<Input
type="text"
:placeholder="t('placeholders.webhookEventName')"
:modelValue="action.value[1] || ''"
@update:modelValue="(value) => handleWebhookEventChange(value, index)"
/>
<p class="text-xs text-muted-foreground mt-1">
{{ $t('admin.automation.webhookEventNameHint') }}
</p>
</div>
</div>

<div
class="flex-1 min-w-0"
v-if="action.type && conversationActions[action.type]?.type === 'text'"
>
<Input
type="text"
:placeholder="placeholderForText(action.type)"
:modelValue="action.value[0] || ''"
@update:modelValue="(value) => handleTextChange(value, index)"
/>
</div>
</div>

<CloseButton :onClose="() => removeAction(index)" />
Expand Down Expand Up @@ -84,10 +145,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 '../../../stores/tag'
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,
Expand All @@ -97,7 +163,7 @@ import {
SelectValue
} from '@shared-ui/components/ui/select'
import { SelectTag } from '@shared-ui/components/ui/select'
import { useConversationFilters } from '../../../composables/useConversationFilters'
import { useConversationFilters } from '@main/composables/useConversationFilters'
import { getTextFromHTML } from '@shared-ui/utils/string'
import { useI18n } from 'vue-i18n'
import Editor from '@main/components/editor/TextEditor.vue'
Expand All @@ -114,11 +180,73 @@ const { actions } = toRefs(props)
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 notifyRecipientOptions = computed(() => [
{ label: t('globals.terms.assignee'), value: 'assignee' },
{ label: t('globals.terms.assignedTeam'), 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)
}

const handleTextChange = (value, index) => {
actions.value[index].value = [value]
emitUpdate(index)
}

const handleWebhookChange = (value, index) => {
if (typeof value === 'object') {
value = value.value
}
const current = actions.value[index].value || []
actions.value[index].value = [value || '', current[1] || '']
emitUpdate(index)
}

const handleWebhookEventChange = (value, index) => {
const current = actions.value[index].value || []
actions.value[index].value = [current[0] || '', value]
emitUpdate(index)
}

const placeholderForText = (type) => {
if (type === 'snooze') return t('placeholders.snoozeDuration')
return t('actions.setValue')
}

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)
}

Expand Down
11 changes: 8 additions & 3 deletions frontend/apps/main/src/features/admin/automation/RuleBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading