diff --git a/cmd/conversation.go b/cmd/conversation.go index d8c7d4af..cdb900de 100644 --- a/cmd/conversation.go +++ b/cmd/conversation.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "strconv" - "strings" "time" amodels "github.com/abhinavxd/libredesk/internal/auth/models" @@ -11,6 +10,7 @@ import ( "github.com/abhinavxd/libredesk/internal/automation/models" cmodels "github.com/abhinavxd/libredesk/internal/conversation/models" "github.com/abhinavxd/libredesk/internal/envelope" + medModels "github.com/abhinavxd/libredesk/internal/media/models" "github.com/abhinavxd/libredesk/internal/stringutil" umodels "github.com/abhinavxd/libredesk/internal/user/models" "github.com/valyala/fasthttp" @@ -18,6 +18,18 @@ import ( "github.com/zerodha/fastglue" ) +type createConversationRequest struct { + InboxID int `json:"inbox_id" form:"inbox_id"` + AssignedAgentID int `json:"agent_id" form:"agent_id"` + AssignedTeamID int `json:"team_id" form:"team_id"` + Email string `json:"contact_email" form:"contact_email"` + FirstName string `json:"first_name" form:"first_name"` + LastName string `json:"last_name" form:"last_name"` + Subject string `json:"subject" form:"subject"` + Content string `json:"content" form:"content"` + Attachments []int `json:"attachments" form:"attachments"` +} + // handleGetAllConversations retrieves all conversations. func handleGetAllConversations(r *fastglue.Request) error { var ( @@ -632,36 +644,32 @@ func filterCurrentConv(convs []cmodels.Conversation, uuid string) []cmodels.Conv // handleCreateConversation creates a new conversation and sends a message to it. func handleCreateConversation(r *fastglue.Request) error { var ( - app = r.Context.(*App) - auser = r.RequestCtx.UserValue("user").(amodels.User) - inboxID = r.RequestCtx.PostArgs().GetUintOrZero("inbox_id") - assignedAgentID = r.RequestCtx.PostArgs().GetUintOrZero("agent_id") - assignedTeamID = r.RequestCtx.PostArgs().GetUintOrZero("team_id") - email = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("contact_email"))) - firstName = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("first_name"))) - lastName = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("last_name"))) - subject = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("subject"))) - content = strings.TrimSpace(string(r.RequestCtx.PostArgs().Peek("content"))) - to = []string{email} + app = r.Context.(*App) + auser = r.RequestCtx.UserValue("user").(amodels.User) + req = createConversationRequest{} ) + if err := r.Decode(&req, "json"); err != nil { + app.lo.Error("error decoding create conversation request", "error", err) + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError) + } + + to := []string{req.Email} + // Validate required fields - if inboxID <= 0 { - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`inbox_id`"), nil, envelope.InputError) + if req.InboxID <= 0 { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`inbox_id`"), nil, envelope.InputError) } - if subject == "" { - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`subject`"), nil, envelope.InputError) + if req.Content == "" { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`content`"), nil, envelope.InputError) } - if content == "" { - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`content`"), nil, envelope.InputError) + if req.Email == "" { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`contact_email`"), nil, envelope.InputError) } - if email == "" { - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`contact_email`"), nil, envelope.InputError) + if req.FirstName == "" { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`first_name`"), nil, envelope.InputError) } - if firstName == "" { - return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.fieldRequired", "name", "`first_name`"), nil, envelope.InputError) - } - if !stringutil.ValidEmail(email) { + if !stringutil.ValidEmail(req.Email) { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`contact_email`"), nil, envelope.InputError) } @@ -671,7 +679,7 @@ func handleCreateConversation(r *fastglue.Request) error { } // Check if inbox exists and is enabled. - inbox, err := app.inbox.GetDBRecord(inboxID) + inbox, err := app.inbox.GetDBRecord(req.InboxID) if err != nil { return sendErrorEnvelope(r, err) } @@ -681,11 +689,11 @@ func handleCreateConversation(r *fastglue.Request) error { // Find or create contact. contact := umodels.User{ - Email: null.StringFrom(email), - SourceChannelID: null.StringFrom(email), - FirstName: firstName, - LastName: lastName, - InboxID: inboxID, + Email: null.StringFrom(req.Email), + SourceChannelID: null.StringFrom(req.Email), + FirstName: req.FirstName, + LastName: req.LastName, + InboxID: req.InboxID, } if err := app.user.CreateContact(&contact); err != nil { return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.contact}"), nil)) @@ -695,10 +703,10 @@ func handleCreateConversation(r *fastglue.Request) error { conversationID, conversationUUID, err := app.conversation.CreateConversation( contact.ID, contact.ContactChannelID, - inboxID, + req.InboxID, "", /** last_message **/ time.Now(), /** last_message_at **/ - subject, + req.Subject, true, /** append reference number to subject **/ ) if err != nil { @@ -706,8 +714,19 @@ func handleCreateConversation(r *fastglue.Request) error { return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.conversation}"), nil)) } + // Prepare attachments. + var media = make([]medModels.Media, 0, len(req.Attachments)) + for _, id := range req.Attachments { + m, err := app.media.Get(id, "") + if err != nil { + app.lo.Error("error fetching media", "error", err) + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.media}"), nil, envelope.GeneralError) + } + media = append(media, m) + } + // Send reply to the created conversation. - 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 { + 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 { // Delete the conversation if reply fails. if err := app.conversation.DeleteConversation(conversationUUID); err != nil { app.lo.Error("error deleting conversation", "error", err) @@ -716,11 +735,11 @@ func handleCreateConversation(r *fastglue.Request) error { } // Assign the conversation to the agent or team. - if assignedAgentID > 0 { - app.conversation.UpdateConversationUserAssignee(conversationUUID, assignedAgentID, user) + if req.AssignedAgentID > 0 { + app.conversation.UpdateConversationUserAssignee(conversationUUID, req.AssignedAgentID, user) } - if assignedTeamID > 0 { - app.conversation.UpdateConversationTeamAssignee(conversationUUID, assignedTeamID, user) + if req.AssignedTeamID > 0 { + app.conversation.UpdateConversationTeamAssignee(conversationUUID, req.AssignedTeamID, user) } // Send the created conversation back to the client. diff --git a/cmd/macro.go b/cmd/macro.go index cb1e4142..22589c80 100644 --- a/cmd/macro.go +++ b/cmd/macro.go @@ -81,8 +81,7 @@ func handleCreateMacro(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } - err := app.macro.Create(macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.Actions) - if err != nil { + if err := app.macro.Create(macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions); err != nil { return sendErrorEnvelope(r, err) } @@ -110,7 +109,7 @@ func handleUpdateMacro(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } - if err = app.macro.Update(id, macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.Actions); err != nil { + if err = app.macro.Update(id, macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions); err != nil { return sendErrorEnvelope(r, err) } @@ -275,13 +274,17 @@ func validateMacro(app *App, macro models.Macro) error { return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil) } + if len(macro.VisibleWhen) == 0 { + return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`visible_when`"), nil) + } + var act []autoModels.RuleAction if err := json.Unmarshal(macro.Actions, &act); err != nil { return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.macroAction}"), nil) } for _, a := range act { if len(a.Value) == 0 { - return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.emptyActionValue", "name", a.Type), nil) + return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", a.Type), nil) } } return nil diff --git a/cmd/messages.go b/cmd/messages.go index 0c6406f7..712c97d3 100644 --- a/cmd/messages.go +++ b/cmd/messages.go @@ -132,7 +132,6 @@ func handleSendMessage(r *fastglue.Request) error { app = r.Context.(*App) auser = r.RequestCtx.UserValue("user").(amodels.User) cuuid = r.RequestCtx.UserValue("cuuid").(string) - media = []medModels.Media{} req = messageReq{} ) @@ -153,6 +152,7 @@ func handleSendMessage(r *fastglue.Request) error { } // Prepare attachments. + var media = make([]medModels.Media, 0, len(req.Attachments)) for _, id := range req.Attachments { m, err := app.media.Get(id, "") if err != nil { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 81f21940..b75fa2da 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -106,7 +106,7 @@ - + diff --git a/frontend/src/components/combobox/SelectCombobox.vue b/frontend/src/components/combobox/SelectCombobox.vue new file mode 100644 index 00000000..aaa0e47a --- /dev/null +++ b/frontend/src/components/combobox/SelectCombobox.vue @@ -0,0 +1,61 @@ + + + diff --git a/frontend/src/features/conversation/ConversationTextEditor.vue b/frontend/src/components/editor/TextEditor.vue similarity index 97% rename from frontend/src/features/conversation/ConversationTextEditor.vue rename to frontend/src/components/editor/TextEditor.vue index b9a50bd9..3b48fb44 100644 --- a/frontend/src/features/conversation/ConversationTextEditor.vue +++ b/frontend/src/components/editor/TextEditor.vue @@ -91,7 +91,7 @@ diff --git a/frontend/src/features/conversation/MacroActionsPreview.vue b/frontend/src/features/conversation/MacroActionsPreview.vue index 00e90fc5..5f3931cd 100644 --- a/frontend/src/features/conversation/MacroActionsPreview.vue +++ b/frontend/src/features/conversation/MacroActionsPreview.vue @@ -25,7 +25,7 @@ @@ -54,7 +54,7 @@ import EmojiPicker from 'vue3-emoji-picker' import 'vue3-emoji-picker/css' const attachmentInput = ref(null) -const inlineImageInput = ref(null) +// const inlineImageInput = ref(null) const isEmojiPickerVisible = ref(false) const emojiPickerRef = ref(null) const emit = defineEmits(['emojiSelect']) @@ -65,6 +65,7 @@ defineProps({ isSending: Boolean, enableSend: Boolean, handleSend: Function, + showSendButton: Boolean, handleFileUpload: Function, handleInlineImageUpload: Function }) diff --git a/frontend/src/features/conversation/message/attachment/AttachmentsPreview.vue b/frontend/src/features/conversation/message/attachment/AttachmentsPreview.vue index 68b1c44d..be9adcff 100644 --- a/frontend/src/features/conversation/message/attachment/AttachmentsPreview.vue +++ b/frontend/src/features/conversation/message/attachment/AttachmentsPreview.vue @@ -29,7 +29,7 @@