Skip to content

Commit 2e1188e

Browse files
authored
Merge pull request #100 from abhinavxd/feat/allow-macro-in-new-conversations
Feat: Allow setting macro in new conversations along with attachments
2 parents f43acb7 + afeec39 commit 2e1188e

40 files changed

+1122
-975
lines changed

cmd/conversation.go

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,33 @@ package main
33
import (
44
"encoding/json"
55
"strconv"
6-
"strings"
76
"time"
87

98
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
109
authzModels "github.com/abhinavxd/libredesk/internal/authz/models"
1110
"github.com/abhinavxd/libredesk/internal/automation/models"
1211
cmodels "github.com/abhinavxd/libredesk/internal/conversation/models"
1312
"github.com/abhinavxd/libredesk/internal/envelope"
13+
medModels "github.com/abhinavxd/libredesk/internal/media/models"
1414
"github.com/abhinavxd/libredesk/internal/stringutil"
1515
umodels "github.com/abhinavxd/libredesk/internal/user/models"
1616
"github.com/valyala/fasthttp"
1717
"github.com/volatiletech/null/v9"
1818
"github.com/zerodha/fastglue"
1919
)
2020

21+
type createConversationRequest struct {
22+
InboxID int `json:"inbox_id" form:"inbox_id"`
23+
AssignedAgentID int `json:"agent_id" form:"agent_id"`
24+
AssignedTeamID int `json:"team_id" form:"team_id"`
25+
Email string `json:"contact_email" form:"contact_email"`
26+
FirstName string `json:"first_name" form:"first_name"`
27+
LastName string `json:"last_name" form:"last_name"`
28+
Subject string `json:"subject" form:"subject"`
29+
Content string `json:"content" form:"content"`
30+
Attachments []int `json:"attachments" form:"attachments"`
31+
}
32+
2133
// handleGetAllConversations retrieves all conversations.
2234
func handleGetAllConversations(r *fastglue.Request) error {
2335
var (
@@ -632,36 +644,32 @@ func filterCurrentConv(convs []cmodels.Conversation, uuid string) []cmodels.Conv
632644
// handleCreateConversation creates a new conversation and sends a message to it.
633645
func handleCreateConversation(r *fastglue.Request) error {
634646
var (
635-
app = r.Context.(*App)
636-
auser = r.RequestCtx.UserValue("user").(amodels.User)
637-
inboxID = r.RequestCtx.PostArgs().GetUintOrZero("inbox_id")
638-
assignedAgentID = r.RequestCtx.PostArgs().GetUintOrZero("agent_id")
639-
assignedTeamID = r.RequestCtx.PostArgs().GetUintOrZero("team_id")
640-
email = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("contact_email")))
641-
firstName = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("first_name")))
642-
lastName = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("last_name")))
643-
subject = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("subject")))
644-
content = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("content")))
645-
to = []string{email}
647+
app = r.Context.(*App)
648+
auser = r.RequestCtx.UserValue("user").(amodels.User)
649+
req = createConversationRequest{}
646650
)
647651

648-
// Validate required fields
649-
if inboxID <= 0 {
650-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`inbox_id`"), nil, envelope.InputError)
652+
if err := r.Decode(&req, "json"); err != nil {
653+
app.lo.Error("error decoding create conversation request", "error", err)
654+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
651655
}
652-
if subject == "" {
653-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`subject`"), nil, envelope.InputError)
656+
657+
to := []string{req.Email}
658+
659+
// Validate required fields
660+
if req.InboxID <= 0 {
661+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`inbox_id`"), nil, envelope.InputError)
654662
}
655-
if content == "" {
656-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`content`"), nil, envelope.InputError)
663+
if req.Content == "" {
664+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`content`"), nil, envelope.InputError)
657665
}
658-
if email == "" {
659-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`contact_email`"), nil, envelope.InputError)
666+
if req.Email == "" {
667+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`contact_email`"), nil, envelope.InputError)
660668
}
661-
if firstName == "" {
662-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`first_name`"), nil, envelope.InputError)
669+
if req.FirstName == "" {
670+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`first_name`"), nil, envelope.InputError)
663671
}
664-
if !stringutil.ValidEmail(email) {
672+
if !stringutil.ValidEmail(req.Email) {
665673
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`contact_email`"), nil, envelope.InputError)
666674
}
667675

@@ -671,7 +679,7 @@ func handleCreateConversation(r *fastglue.Request) error {
671679
}
672680

673681
// Check if inbox exists and is enabled.
674-
inbox, err := app.inbox.GetDBRecord(inboxID)
682+
inbox, err := app.inbox.GetDBRecord(req.InboxID)
675683
if err != nil {
676684
return sendErrorEnvelope(r, err)
677685
}
@@ -681,11 +689,11 @@ func handleCreateConversation(r *fastglue.Request) error {
681689

682690
// Find or create contact.
683691
contact := umodels.User{
684-
Email: null.StringFrom(email),
685-
SourceChannelID: null.StringFrom(email),
686-
FirstName: firstName,
687-
LastName: lastName,
688-
InboxID: inboxID,
692+
Email: null.StringFrom(req.Email),
693+
SourceChannelID: null.StringFrom(req.Email),
694+
FirstName: req.FirstName,
695+
LastName: req.LastName,
696+
InboxID: req.InboxID,
689697
}
690698
if err := app.user.CreateContact(&contact); err != nil {
691699
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.contact}"), nil))
@@ -695,19 +703,30 @@ func handleCreateConversation(r *fastglue.Request) error {
695703
conversationID, conversationUUID, err := app.conversation.CreateConversation(
696704
contact.ID,
697705
contact.ContactChannelID,
698-
inboxID,
706+
req.InboxID,
699707
"", /** last_message **/
700708
time.Now(), /** last_message_at **/
701-
subject,
709+
req.Subject,
702710
true, /** append reference number to subject **/
703711
)
704712
if err != nil {
705713
app.lo.Error("error creating conversation", "error", err)
706714
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.conversation}"), nil))
707715
}
708716

717+
// Prepare attachments.
718+
var media = make([]medModels.Media, 0, len(req.Attachments))
719+
for _, id := range req.Attachments {
720+
m, err := app.media.Get(id, "")
721+
if err != nil {
722+
app.lo.Error("error fetching media", "error", err)
723+
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.media}"), nil, envelope.GeneralError)
724+
}
725+
media = append(media, m)
726+
}
727+
709728
// Send reply to the created conversation.
710-
if err := app.conversation.SendReply(nil /**media**/, inboxID, auser.ID /**sender_id**/, conversationUUID, content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil {
729+
if err := app.conversation.SendReply(media, req.InboxID, auser.ID /**sender_id**/, conversationUUID, req.Content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil {
711730
// Delete the conversation if reply fails.
712731
if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
713732
app.lo.Error("error deleting conversation", "error", err)
@@ -716,11 +735,11 @@ func handleCreateConversation(r *fastglue.Request) error {
716735
}
717736

718737
// Assign the conversation to the agent or team.
719-
if assignedAgentID > 0 {
720-
app.conversation.UpdateConversationUserAssignee(conversationUUID, assignedAgentID, user)
738+
if req.AssignedAgentID > 0 {
739+
app.conversation.UpdateConversationUserAssignee(conversationUUID, req.AssignedAgentID, user)
721740
}
722-
if assignedTeamID > 0 {
723-
app.conversation.UpdateConversationTeamAssignee(conversationUUID, assignedTeamID, user)
741+
if req.AssignedTeamID > 0 {
742+
app.conversation.UpdateConversationTeamAssignee(conversationUUID, req.AssignedTeamID, user)
724743
}
725744

726745
// Send the created conversation back to the client.

cmd/macro.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ func handleCreateMacro(r *fastglue.Request) error {
8181
return sendErrorEnvelope(r, err)
8282
}
8383

84-
err := app.macro.Create(macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.Actions)
85-
if err != nil {
84+
if err := app.macro.Create(macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions); err != nil {
8685
return sendErrorEnvelope(r, err)
8786
}
8887

@@ -110,7 +109,7 @@ func handleUpdateMacro(r *fastglue.Request) error {
110109
return sendErrorEnvelope(r, err)
111110
}
112111

113-
if err = app.macro.Update(id, macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.Actions); err != nil {
112+
if err = app.macro.Update(id, macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions); err != nil {
114113
return sendErrorEnvelope(r, err)
115114
}
116115

@@ -275,13 +274,17 @@ func validateMacro(app *App, macro models.Macro) error {
275274
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil)
276275
}
277276

277+
if len(macro.VisibleWhen) == 0 {
278+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`visible_when`"), nil)
279+
}
280+
278281
var act []autoModels.RuleAction
279282
if err := json.Unmarshal(macro.Actions, &act); err != nil {
280283
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.macroAction}"), nil)
281284
}
282285
for _, a := range act {
283286
if len(a.Value) == 0 {
284-
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.emptyActionValue", "name", a.Type), nil)
287+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", a.Type), nil)
285288
}
286289
}
287290
return nil

cmd/messages.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ func handleSendMessage(r *fastglue.Request) error {
132132
app = r.Context.(*App)
133133
auser = r.RequestCtx.UserValue("user").(amodels.User)
134134
cuuid = r.RequestCtx.UserValue("cuuid").(string)
135-
media = []medModels.Media{}
136135
req = messageReq{}
137136
)
138137

@@ -153,6 +152,7 @@ func handleSendMessage(r *fastglue.Request) error {
153152
}
154153

155154
// Prepare attachments.
155+
var media = make([]medModels.Media, 0, len(req.Attachments))
156156
for _, id := range req.Attachments {
157157
m, err := app.media.Get(id, "")
158158
if err != nil {

frontend/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
<Command />
107107

108108
<!-- Create conversation dialog -->
109-
<CreateConversation v-model="openCreateConversationDialog" />
109+
<CreateConversation v-model="openCreateConversationDialog" v-if="openCreateConversationDialog" />
110110
</template>
111111

112112
<script setup>

frontend/src/api/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,11 @@ const updateConversationCustomAttribute = (uuid, data) => http.put(`/api/v1/conv
231231
'Content-Type': 'application/json'
232232
}
233233
})
234-
const createConversation = (data) => http.post('/api/v1/conversations', data)
234+
const createConversation = (data) => http.post('/api/v1/conversations', data, {
235+
headers: {
236+
'Content-Type': 'application/json'
237+
}
238+
})
235239
const updateConversationStatus = (uuid, data) => http.put(`/api/v1/conversations/${uuid}/status`, data)
236240
const updateConversationPriority = (uuid, data) => http.put(`/api/v1/conversations/${uuid}/priority`, data)
237241
const updateAssigneeLastSeen = (uuid) => http.put(`/api/v1/conversations/${uuid}/last-seen`)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<template>
2+
<Button
3+
variant="ghost"
4+
@click.prevent="onClose"
5+
size="xs"
6+
class="text-gray-400 dark:text-gray-500 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 w-6 h-6 p-0"
7+
>
8+
<slot>
9+
<X size="16" />
10+
</slot>
11+
</Button>
12+
</template>
13+
14+
<script setup>
15+
import { Button } from '@/components/ui/button'
16+
import { X } from 'lucide-vue-next'
17+
18+
defineProps({
19+
onClose: {
20+
type: Function,
21+
required: true
22+
}
23+
})
24+
</script>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<template>
2+
<ComboBox
3+
:model-value="normalizedValue"
4+
@update:model-value="$emit('update:modelValue', $event)"
5+
:items="items"
6+
:placeholder="placeholder"
7+
>
8+
<!-- Items -->
9+
<template #item="{ item }">
10+
<div class="flex items-center gap-2">
11+
<!--USER -->
12+
<Avatar v-if="type === 'user'" class="w-7 h-7">
13+
<AvatarImage :src="item.avatar_url || ''" :alt="item.label.slice(0, 2)" />
14+
<AvatarFallback>{{ item.label.slice(0, 2).toUpperCase() }}</AvatarFallback>
15+
</Avatar>
16+
17+
<!-- Others -->
18+
<span v-else-if="item.emoji">{{ item.emoji }}</span>
19+
<span>{{ item.label }}</span>
20+
</div>
21+
</template>
22+
23+
<!-- Selected -->
24+
<template #selected="{ selected }">
25+
<div class="flex items-center gap-2">
26+
<div v-if="selected" class="flex items-center gap-2">
27+
<!--USER -->
28+
<Avatar v-if="type === 'user'" class="w-7 h-7">
29+
<AvatarImage :src="selected.avatar_url || ''" :alt="selected.label.slice(0, 2)" />
30+
<AvatarFallback>{{ selected.label.slice(0, 2).toUpperCase() }}</AvatarFallback>
31+
</Avatar>
32+
33+
<!-- Others -->
34+
<span v-else-if="selected.emoji">{{ selected.emoji }}</span>
35+
<span>{{ selected.label }}</span>
36+
</div>
37+
<span v-else>{{ placeholder }}</span>
38+
</div>
39+
</template>
40+
</ComboBox>
41+
</template>
42+
43+
<script setup>
44+
import { computed } from 'vue'
45+
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
46+
import ComboBox from '@/components/ui/combobox/ComboBox.vue'
47+
48+
const props = defineProps({
49+
modelValue: [String, Number, Object],
50+
placeholder: String,
51+
items: Array,
52+
type: {
53+
type: String
54+
}
55+
})
56+
57+
// Convert to str.
58+
const normalizedValue = computed(() => String(props.modelValue || ''))
59+
60+
defineEmits(['update:modelValue'])
61+
</script>

frontend/src/features/conversation/ConversationTextEditor.vue renamed to frontend/src/components/editor/TextEditor.vue

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
</template>
9292

9393
<script setup>
94-
import { ref, watch, watchEffect, onUnmounted } from 'vue'
94+
import { ref, watch, watchEffect, onUnmounted, computed } from 'vue'
9595
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/vue-3'
9696
import {
9797
ChevronDown,
@@ -136,6 +136,10 @@ const props = defineProps({
136136
setInlineImage: Object,
137137
insertContent: String,
138138
clearContent: Boolean,
139+
autoFocus: {
140+
type: Boolean,
141+
default: true
142+
},
139143
aiPrompts: {
140144
type: Array,
141145
default: () => []
@@ -188,7 +192,7 @@ const CustomTableHeader = TableHeader.extend({
188192
}
189193
})
190194
191-
const editorConfig = {
195+
const editorConfig = computed(() => ({
192196
extensions: [
193197
StarterKit.configure(),
194198
Image.configure({ HTMLAttributes: { class: 'inline-image' } }),
@@ -201,7 +205,7 @@ const editorConfig = {
201205
CustomTableCell,
202206
CustomTableHeader
203207
],
204-
autofocus: true,
208+
autofocus: props.autoFocus,
205209
editorProps: {
206210
attributes: { class: 'outline-none' },
207211
handleKeyDown: (view, event) => {
@@ -210,17 +214,17 @@ const editorConfig = {
210214
return true
211215
}
212216
if (event.ctrlKey && event.key.toLowerCase() === 'b') {
213-
// Prevent outer listeners
217+
// Prevent outer listeners
214218
event.stopPropagation()
215219
return false
216220
}
217221
}
218222
}
219-
}
223+
}))
220224
221225
const editor = ref(
222226
useEditor({
223-
...editorConfig,
227+
...editorConfig.value,
224228
content: htmlContent.value,
225229
onSelectionUpdate: ({ editor }) => {
226230
const { from, to } = editor.state.selection

0 commit comments

Comments
 (0)