fix: whitespace being trimmed in canned repsonse

- refactor: conversations store.
- feat: Show conversation subject on top of conversation messages list
- feat: adds shadow to panels
- update: schema.sql adds insert statements for priority / status
- fix: trim canned response content to 100 chars.
- revert: rename team conversations tab to unassigned.
This commit is contained in:
Abhinav Raut
2024-10-11 05:31:12 +05:30
parent 7e15995048
commit fbf631d8ad
33 changed files with 247 additions and 289 deletions

View File

@@ -17,12 +17,10 @@ func handleGetAllConversations(r *fastglue.Request) error {
app = r.Context.(*App)
order = string(r.RequestCtx.QueryArgs().Peek("order"))
orderBy = string(r.RequestCtx.QueryArgs().Peek("order_by"))
filter = string(r.RequestCtx.QueryArgs().Peek("filter"))
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
)
// Fetch all conversations
c, err := app.conversation.GetAllConversations(order, orderBy, filter, page, pageSize)
c, err := app.conversation.GetAllConversationsList(order, orderBy, page, pageSize)
if err != nil {
return sendErrorEnvelope(r, err)
}
@@ -36,31 +34,27 @@ func handleGetAssignedConversations(r *fastglue.Request) error {
user = r.RequestCtx.UserValue("user").(umodels.User)
order = string(r.RequestCtx.QueryArgs().Peek("order"))
orderBy = string(r.RequestCtx.QueryArgs().Peek("order_by"))
filter = string(r.RequestCtx.QueryArgs().Peek("filter"))
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
)
// Fetch conversations assigned to the user
c, err := app.conversation.GetAssignedConversations(user.ID, order, orderBy, filter, page, pageSize)
c, err := app.conversation.GetAssignedConversationsList(user.ID, order, orderBy, page, pageSize)
if err != nil {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "")
}
return r.SendEnvelope(c)
}
// handleGetTeamConversations retrieves team-specific conversations.
func handleGetTeamConversations(r *fastglue.Request) error {
// handleGetUnassignedConversations retrieves unassigned conversations.
func handleGetUnassignedConversations(r *fastglue.Request) error {
var (
app = r.Context.(*App)
user = r.RequestCtx.UserValue("user").(umodels.User)
order = string(r.RequestCtx.QueryArgs().Peek("order"))
orderBy = string(r.RequestCtx.QueryArgs().Peek("order_by"))
filter = string(r.RequestCtx.QueryArgs().Peek("filter"))
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
)
// Fetch conversations assigned to the user's team
c, err := app.conversation.GetTeamConversations(user.ID, order, orderBy, filter, page, pageSize)
c, err := app.conversation.GetUnassignedConversationsList(user.ID, order, orderBy, page, pageSize)
if err != nil {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "")
}
@@ -74,8 +68,6 @@ func handleGetConversation(r *fastglue.Request) error {
uuid = r.RequestCtx.UserValue("uuid").(string)
user = r.RequestCtx.UserValue("user").(umodels.User)
)
// Check if the user has access to the conversation
conversation, err := enforceConversationAccess(app, uuid, user)
if err != nil {
return sendErrorEnvelope(r, err)
@@ -91,14 +83,10 @@ func handleUpdateConversationAssigneeLastSeen(r *fastglue.Request) error {
uuid = r.RequestCtx.UserValue("uuid").(string)
user = r.RequestCtx.UserValue("user").(umodels.User)
)
// Check permission
_, err := enforceConversationAccess(app, uuid, user)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Update last seen
if err = app.conversation.UpdateConversationAssigneeLastSeen(uuid); err != nil {
return sendErrorEnvelope(r, err)
}
@@ -112,14 +100,10 @@ func handleGetConversationParticipants(r *fastglue.Request) error {
uuid = r.RequestCtx.UserValue("uuid").(string)
user = r.RequestCtx.UserValue("user").(umodels.User)
)
// Check permission
_, err := enforceConversationAccess(app, uuid, user)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Fetch participants
p, err := app.conversation.GetConversationParticipants(uuid)
if err != nil {
return sendErrorEnvelope(r, err)
@@ -139,13 +123,11 @@ func handleUpdateConversationUserAssignee(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid assignee `id`.", nil, envelope.InputError)
}
// Check permission
_, err = enforceConversationAccess(app, uuid, user)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Update the user assignee
if err := app.conversation.UpdateConversationUserAssignee(uuid, assigneeID, user); err != nil {
return sendErrorEnvelope(r, err)
}
@@ -163,14 +145,10 @@ func handleUpdateTeamAssignee(r *fastglue.Request) error {
if err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid assignee `id`.", nil, envelope.InputError)
}
// Check permission
_, err = enforceConversationAccess(app, uuid, user)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Update the team assignee
if err := app.conversation.UpdateConversationTeamAssignee(uuid, assigneeID, user); err != nil {
return sendErrorEnvelope(r, err)
}
@@ -186,14 +164,10 @@ func handleUpdateConversationPriority(r *fastglue.Request) error {
uuid = r.RequestCtx.UserValue("uuid").(string)
user = r.RequestCtx.UserValue("user").(umodels.User)
)
// Check permission
_, err := enforceConversationAccess(app, uuid, user)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Update priority
if err := app.conversation.UpdateConversationPriority(uuid, priority, user); err != nil {
return sendErrorEnvelope(r, err)
}
@@ -209,14 +183,10 @@ func handleUpdateConversationStatus(r *fastglue.Request) error {
uuid = r.RequestCtx.UserValue("uuid").(string)
user = r.RequestCtx.UserValue("user").(umodels.User)
)
// Check permission
_, err := enforceConversationAccess(app, uuid, user)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Update status
if err := app.conversation.UpdateConversationStatus(uuid, status, user); err != nil {
return sendErrorEnvelope(r, err)
}
@@ -241,13 +211,11 @@ func handleAddConversationTags(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "error adding tags", nil, "")
}
// Check permission
_, err = enforceConversationAccess(app, uuid, user)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Upsert tags
if err := app.conversation.UpsertConversationTags(uuid, tagIDs); err != nil {
return sendErrorEnvelope(r, err)
}

View File

@@ -40,7 +40,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
// Conversation and message.
g.GET("/api/conversations/all", perm(handleGetAllConversations, "conversations", "read_all"))
g.GET("/api/conversations/team", perm(handleGetTeamConversations, "conversations", "read_team"))
g.GET("/api/conversations/unassigned", perm(handleGetUnassignedConversations, "conversations", "read_unassigned"))
g.GET("/api/conversations/assigned", perm(handleGetAssignedConversations, "conversations", "read_assigned"))
g.PUT("/api/conversations/{uuid}/assignee/user", perm(handleUpdateConversationUserAssignee, "conversations", "update_user_assignee"))

View File

@@ -78,6 +78,7 @@ func main() {
// Installer.
if ko.Bool("install") {
install(db, fs)
setSystemUserPass(db)
os.Exit(0)
}

View File

@@ -1,10 +1,10 @@
<template>
<Toaster />
<TooltipProvider :delay-duration="200">
<div class="bg-background text-foreground">
<div>
<div v-if="$route.path !== '/'">
<ResizablePanelGroup direction="horizontal" auto-save-id="app.vue.resizable.panel">
<ResizablePanel id="resize-panel-1" collapsible :default-size="10" :collapsed-size="1" :min-size="7"
<ResizablePanel class="shadow shadow-gray-300" id="resize-panel-1" collapsible :default-size="10" :collapsed-size="1" :min-size="7"
:max-size="20" :class="cn(isCollapsed && 'min-w-[50px] transition-all duration-200 ease-in-out')"
@expand="toggleNav(false)" @collapse="toggleNav(true)">
<NavBar :is-collapsed="isCollapsed" :links="navLinks" :bottom-links="bottomLinks" />

View File

@@ -119,9 +119,9 @@ const updateAssignee = (uuid, assignee_type, data) =>
const updateConversationStatus = (uuid, data) => http.put(`/api/conversations/${uuid}/status`, data)
const updateConversationPriority = (uuid, data) => http.put(`/api/conversations/${uuid}/priority`, data)
const updateAssigneeLastSeen = (uuid) => http.put(`/api/conversations/${uuid}/last-seen`)
const getMessage = (cuuid, uuid) => http.get(`/api/conversations/${cuuid}/messages/${uuid}`)
const getConversationMessage = (cuuid, uuid) => http.get(`/api/conversations/${cuuid}/messages/${uuid}`)
const retryMessage = (cuuid, uuid) => http.put(`/api/conversations/${cuuid}/messages/${uuid}/retry`)
const getMessages = (uuid, page) =>
const getConversationMessages = (uuid, page) =>
http.get(`/api/conversations/${uuid}/messages`, {
params: { page: page }
})
@@ -137,12 +137,12 @@ const getCannedResponses = () => http.get('/api/canned-responses')
const createCannedResponse = (data) => http.post('/api/canned-responses', data)
const updateCannedResponse = (id, data) => http.put(`/api/canned-responses/${id}`, data)
const deleteCannedResponse = (id) => http.delete(`/api/canned-responses/${id}`)
const getAssignedConversations = (page, filter) =>
http.get(`/api/conversations/assigned?page=${page}&filter=${filter}`)
const getTeamConversations = (page, filter) =>
http.get(`/api/conversations/team?page=${page}&filter=${filter}`)
const getAllConversations = (page, filter) =>
http.get(`/api/conversations/all?page=${page}&filter=${filter}`)
const getAssignedConversations = (page) =>
http.get(`/api/conversations/assigned?page=${page}`)
const getUnassignedConversations = (page) =>
http.get(`/api/conversations/unassigned?page=${page}`)
const getAllConversations = (page) =>
http.get(`/api/conversations/all?page=${page}`)
const uploadMedia = (data) =>
http.post('/api/media', data, {
headers: {
@@ -204,15 +204,15 @@ export default {
getAutomationRule,
getAutomationRules,
getAssignedConversations,
getTeamConversations,
getUnassignedConversations,
getAllConversations,
getGlobalDashboardCharts,
getGlobalDashboardCounts,
getUserDashboardCounts,
getUserDashboardCharts,
getConversationParticipants,
getMessage,
getMessages,
getConversationMessage,
getConversationMessages,
getCurrentUser,
getCannedResponses,
createCannedResponse,

View File

@@ -166,10 +166,6 @@ body {
@apply text-muted-foreground text-sm;
}
.text-xs-muted {
@apply text-muted-foreground text-xs;
}
.box {
@apply border shadow;
}

View File

@@ -4,10 +4,10 @@
@click="handleClick"
>
<div class="flex items-center mb-4">
<component :is="icon" size="16" class="mr-2" />
<component :is="icon" size="25" class="mr-2" />
<p class="text-lg">{{ title }}</p>
</div>
<p class="text-xs-muted">{{ subTitle }}</p>
<p class="text-sm text-muted-foreground">{{ subTitle }}</p>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col space-y-1">
<span class="text-2xl">{{ title }}</span>
<p class="text-xs-muted">{{ description }}</p>
<p class="text-muted-foreground text-lg">{{ description }}</p>
</div>
</template>

View File

@@ -76,9 +76,9 @@ const permissions = ref([
{
name: 'Conversation',
permissions: [
{ name: 'conversations:read_all', label: 'View all conversations list' },
{ name: 'conversations:read_team', label: 'View team conversations list' },
{ name: 'conversations:read_assigned', label: 'View assigned conversations list' },
{ name: 'conversations:read_all', label: 'View all conversations' },
{ name: 'conversations:read_unassigned', label: 'View unassigned conversations' },
{ name: 'conversations:read_assigned', label: 'View assigned conversations' },
{ name: 'conversations:read', label: 'View conversation' },
{ name: 'conversations:update_user_assignee', label: 'Edit assigned user' },
{ name: 'conversations:update_team_assignee', label: 'Edit assigned team' },

View File

@@ -16,7 +16,7 @@
>
<div class="flex flex-col items-start space-y-1">
<span class="text-sm">{{ item.title }}</span>
<p class="text-xs-muted break-words whitespace-normal">{{ item.description }}</p>
<p class="text-muted-foreground text-xs break-words whitespace-normal">{{ item.description }}</p>
</div>
</Button>
</template>

View File

@@ -2,26 +2,18 @@
<div class="relative" v-if="conversationStore.messages.data">
<!-- Header -->
<div class="px-4 border-b h-[47px] flex items-center justify-between">
<div class="px-4 border-b h-[47px] flex items-center justify-between shadow shadow-gray-100">
<div class="flex items-center space-x-3 text-sm">
<div class="font-bold">
{{ contactFullName }}
{{ conversationStore.current.subject }}
</div>
<Tooltip>
<TooltipTrigger>
<Badge :variant="getBadgeVariant">
{{ conversationStore.conversation.data.status }}
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>Status</p>
</TooltipContent>
</Tooltip>
</div>
<div>
<DropdownMenu>
<DropdownMenuTrigger>
<Icon icon="lucide:ellipsis-vertical" class="mt-2 size-6"></Icon>
<Badge :variant="getBadgeVariant">
{{ conversationStore.current.status }}
</Badge>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem v-for="status in statuses" :key="status.name" @click="handleUpdateStatus(status.name)">
@@ -33,8 +25,6 @@
</div>
<!-- Header end -->
<Error class="sticky" :error-message="conversationStore.messages.errorMessage"></Error>
<!-- Messages & reply box -->
<div class="flex flex-col h-screen">
<MessageList class="flex-1" />
@@ -48,7 +38,6 @@
<script setup>
import { computed, ref, onMounted } from 'vue'
import { useConversationStore } from '@/stores/conversation'
import { Error } from '@/components/ui/error'
import { Badge } from '@/components/ui/badge'
import {
DropdownMenu,
@@ -56,11 +45,9 @@ import {
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import MessageList from '@/components/message/MessageList.vue'
import ReplyBox from './ReplyBox.vue'
import api from '@/api'
import { Icon } from '@iconify/vue'
const conversationStore = useConversationStore()
const statuses = ref([])
@@ -75,14 +62,10 @@ const getStatuses = async () => {
}
const getBadgeVariant = computed(() => {
return conversationStore.conversation.data?.status == 'Spam' ? 'destructive' : 'primary'
return conversationStore.current?.status == 'Spam' ? 'destructive' : 'primary'
})
const handleUpdateStatus = (status) => {
conversationStore.updateStatus(status)
}
const contactFullName = computed(() => {
return conversationStore.getContactFullName(conversationStore.conversation.data?.uuid)
})
</script>

View File

@@ -111,7 +111,9 @@ watch(
() => props.contentToSet,
(newContent) => {
if (newContent) {
editor.value.commands.setContent(newContent)
editor.value.commands.setContent(newContent, false, {
preserveWhitespace: "full"
})
editor.value.commands.focus()
emit('contentSet')
}

View File

@@ -8,7 +8,7 @@
'cursor-pointer rounded p-1 hover:bg-secondary',
{ 'bg-secondary': index === selectedResponseIndex }
]" @click="selectCannedResponse(response.content)" @mouseenter="selectedResponseIndex = index">
<span class="font-semibold">{{ response.title }}</span> - {{ response.content }}
<span class="font-semibold">{{ response.title }}</span> - {{ response.content.slice(0, 100) }}...
</li>
</ul>
</div>
@@ -184,7 +184,7 @@ const handleSend = async () => {
try {
// Replace image source url with cid.
const message = transformImageSrcToCID(editorHTML.value)
await api.sendMessage(conversationStore.conversation.data.uuid, {
await api.sendMessage(conversationStore.current.uuid, {
private: messageType.value === 'private_note',
message: message,
attachments: uploadedFiles.value.map((file) => file.id)
@@ -199,7 +199,7 @@ const handleSend = async () => {
clearContent.value = true
uploadedFiles.value = []
}
api.updateAssigneeLastSeen(conversationStore.conversation.data.uuid)
api.updateAssigneeLastSeen(conversationStore.current.uuid)
}
const handleOnFileDelete = (uuid) => {

View File

@@ -13,7 +13,7 @@
<script setup>
defineProps({
icon: {
type: Object,
type: Function,
required: true
},
title: {

View File

@@ -2,9 +2,7 @@
<div class="h-screen">
<!-- Filters -->
<ConversationListFilters v-model:type="conversationType" v-model:filter="conversationFilter"
:handleFilterChange="handleFilterChange">
</ConversationListFilters>
<ConversationListFilters v-model:type="conversationType"></ConversationListFilters>
<!-- Error / Empty list -->
<EmptyList v-if="emptyConversations" title="No conversations found" message="Try adjusting filters."
@@ -13,14 +11,14 @@
:message="conversationStore.conversations.errorMessage" :icon="MessageCircleWarning"></EmptyList>
<div class="h-screen overflow-y-scroll pb-[180px] flex flex-col">
<!-- Item -->
<ConversationListItem />
<!-- List skeleton -->
<div v-if="conversationsLoading">
<ConversationListItemSkeleton v-for="index in 8" :key="index"></ConversationListItemSkeleton>
</div>
<!-- Item -->
<ConversationListItem />
<!-- Load more -->
<div class="flex justify-center items-center mt-5 relative">
<div v-if="conversationStore.conversations.hasMore && !hasErrored && hasConversations">
@@ -40,7 +38,7 @@
import { onMounted, watch, computed, onUnmounted } from 'vue'
import { useConversationStore } from '@/stores/conversation'
import { subscribeConversationsList } from '@/websocket.js'
import { CONVERSATION_LIST_TYPE, CONVERSATION_FILTERS } from '@/constants/conversation'
import { CONVERSATION_LIST_TYPE } from '@/constants/conversation'
import { MessageCircleWarning, MessageCircleQuestion } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import Spinner from '@/components/ui/spinner/Spinner.vue'
@@ -52,15 +50,14 @@ import { useStorage } from '@vueuse/core'
const conversationStore = useConversationStore()
const conversationType = useStorage('conversation_type', CONVERSATION_LIST_TYPE.ASSIGNED)
const conversationFilter = useStorage('conversation_filter', CONVERSATION_FILTERS.ALL)
let listRefreshInterval = null
onMounted(() => {
conversationStore.fetchConversations(conversationType.value, conversationFilter.value)
subscribeConversationsList(conversationType.value, conversationFilter.value)
conversationStore.fetchConversationsList(conversationType.value)
subscribeConversationsList(conversationType.value)
// Refresh list every min.
listRefreshInterval = setInterval(() => {
conversationStore.fetchConversations(conversationType.value, conversationFilter.value, false)
conversationStore.fetchConversationsList(conversationType.value, false)
}, 60000)
})
@@ -69,18 +66,12 @@ onUnmounted(() => {
})
watch(conversationType, (newType) => {
conversationStore.fetchConversations(newType, conversationFilter.value)
subscribeConversationsList(newType, conversationFilter.value)
conversationStore.fetchConversationsList(newType)
subscribeConversationsList(newType)
})
const handleFilterChange = (filter) => {
conversationFilter.value = filter
conversationStore.fetchConversations(conversationType.value, filter)
subscribeConversationsList(conversationType.value, conversationFilter.value)
}
const loadNextPage = () => {
conversationStore.fetchNextConversations(conversationType.value, conversationFilter.value)
conversationStore.fetchNextConversations(conversationType.value)
}
const hasConversations = computed(() => {

View File

@@ -1,14 +1,12 @@
<template>
<div class="flex justify-between px-2 py-2 border-b">
<Tabs v-model="conversationType">
<TabsList class="w-full flex justify-evenly">
<TabsTrigger value="assigned" class="w-full"> Assigned </TabsTrigger>
<TabsTrigger value="team" class="w-full"> Unassigned </TabsTrigger>
<TabsTrigger value="unassigned" class="w-full"> Unassigned </TabsTrigger>
<TabsTrigger value="all" class="w-full"> All </TabsTrigger>
</TabsList>
</Tabs>
<Popover>
<PopoverTrigger as-child>
<div class="flex items-center mr-2">
@@ -17,20 +15,7 @@
</PopoverTrigger>
<PopoverContent class="w-52">
<div>
<Select v-model="conversationFilter">
<SelectTrigger>
<SelectValue placeholder="Select a filter" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="status_all"> All </SelectItem>
<SelectItem value="status_open"> Open </SelectItem>
<SelectItem value="status_processing"> Processing </SelectItem>
<SelectItem value="status_spam"> Spam </SelectItem>
<SelectItem value="status_resolved"> Resolved </SelectItem>
</SelectGroup>
</SelectContent>
</Select>
Work in progress.
</div>
</PopoverContent>
</Popover>
@@ -38,30 +23,10 @@
</template>
<script setup>
import { defineModel, watch } from 'vue'
import { defineModel } from 'vue'
import { ListFilter } from 'lucide-vue-next'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
const conversationType = defineModel('type')
const conversationFilter = defineModel('filter')
const props = defineProps({
handleFilterChange: {
type: Function,
required: true
},
})
watch(conversationFilter, (newValue) => {
props.handleFilterChange(newValue)
})
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex items-center cursor-pointer flex-row hover:bg-slate-50"
:class="{ 'bg-slate-100': conversation.uuid === conversationStore.conversation.data?.uuid }"
:class="{ 'bg-slate-100': conversation.uuid === conversationStore.current?.uuid }"
v-for="conversation in conversationStore.sortedConversations" :key="conversation.uuid"
@click="router.push('/conversations/' + conversation.uuid)">

View File

@@ -53,9 +53,9 @@ const filteredAgents = ref([])
const assignedAgentName = computed(() => {
const assignedUserId = props.conversation.assigned_user_id
if (!assignedUserId) return 'Select agent...'
if (!assignedUserId) return 'Select agent'
const agent = props.agents.find(agent => agent.id === assignedUserId)
return agent ? `${agent.first_name} ${agent.last_name}` : 'Select agent...'
return agent ? `${agent.first_name} ${agent.last_name}` : 'Select agent'
})
const handleFilterAgents = (search) => {

View File

@@ -52,9 +52,9 @@ const filteredTeams = ref([])
const assignedTeamName = computed(() => {
const assignedTeamId = props.conversation.assigned_team_id
if (!assignedTeamId) return 'Select team...'
if (!assignedTeamId) return 'Select team'
const team = props.teams.find(team => team.id === assignedTeamId)
return team ? team.name : 'Select team...'
return team ? team.name : 'Select team'
})
const handleFilterTeams = (search) => {

View File

@@ -1,7 +1,7 @@
<template>
<div class="p-3">
<ConversationSideBarContact :conversation="conversationStore.conversation.data"></ConversationSideBarContact>
<Accordion type="multiple" collapsible class="border-t mt-4">
<ConversationSideBarContact :conversation="conversationStore.current"></ConversationSideBarContact>
<Accordion type="multiple" collapsible class="border-t mt-4" :default-value="['Actions', 'Information']">
<AccordionItem value="Actions">
<AccordionTrigger>
<h4 class="scroll-m-20 text-base font-medium tracking-tight">
@@ -10,16 +10,16 @@
</AccordionTrigger>
<AccordionContent class="space-y-5">
<!-- Agent -->
<AssignAgent :agents="agents" :conversation="conversationStore.conversation.data" :selectAgent="selectAgent">
<AssignAgent :agents="agents" :conversation="conversationStore.current" :selectAgent="selectAgent">
</AssignAgent>
<!-- Team -->
<AssignTeam :teams="teams" :conversation="conversationStore.conversation.data" :selectTeam="selectTeam">
<AssignTeam :teams="teams" :conversation="conversationStore.current" :selectTeam="selectTeam">
</AssignTeam>
<!-- Priority -->
<PriorityChange :priorities="priorities" :conversation="conversationStore.conversation.data"
<PriorityChange :priorities="priorities" :conversation="conversationStore.current"
:selectPriority="selectPriority"></PriorityChange>
<!-- Tags -->
<SelectTag :initialValue="conversationStore.conversation.data.tags" v-model="selectedTags" :items="tags"
<SelectTag :initialValue="conversationStore.current.tags" v-model="selectedTags" :items="tags"
placeHolder="Select tags"></SelectTag>
</AccordionContent>
</AccordionItem>
@@ -30,7 +30,7 @@
</span>
</AccordionTrigger>
<AccordionContent>
<ConversationInfo :conversation="conversationStore.conversation.data"></ConversationInfo>
<ConversationInfo :conversation="conversationStore.current"></ConversationInfo>
</AccordionContent>
</AccordionItem>
</Accordion>
@@ -154,17 +154,17 @@ const handleUpsertTags = () => {
}
const selectAgent = (id) => {
conversationStore.conversation.data.assigned_user_id = id
conversationStore.current.assigned_user_id = id
handleAssignedUserChange(id)
}
const selectTeam = (id) => {
conversationStore.conversation.data.assigned_team_id = id
conversationStore.current.assigned_team_id = id
handleAssignedTeamChange(id)
}
const selectPriority = (priority) => {
conversationStore.conversation.data.priority = priority
conversationStore.current.priority = priority
handlePriorityChange(priority)
}
</script>

View File

@@ -1,14 +1,8 @@
<template>
<LineChart
:data="renamedData"
index="date"
:categories="['New conversations']"
:y-formatter="
(tick) => {
return tick
}
"
/>
<LineChart :data="chartData" index="date" :categories="['New conversations']" :y-formatter="(tick) => {
return tick
}
" />
</template>
<script setup>
@@ -22,10 +16,10 @@ const props = defineProps({
}
})
const renamedData = computed(() =>
props.data.map((item) => ({
...item,
'New conversations': item.new_conversations
const chartData = computed(() =>
props.data.map(item => ({
date: item.date,
"New conversations": item.new_conversations
}))
)
</script>

View File

@@ -107,6 +107,6 @@ const avatarFallback = computed(() => {
})
const retryMessage = (msg) => {
api.retryMessage(convStore.conversation.data.uuid, msg.uuid)
api.retryMessage(convStore.current.uuid, msg.uuid)
}
</script>

View File

@@ -51,7 +51,7 @@ const props = defineProps({
const convStore = useConversationStore()
const getAvatar = computed(() => {
return convStore.conversation.data.avatar_url ? convStore.conversation.avatar_url : ''
return convStore.current.avatar_url ? convStore.conversation.avatar_url : ''
})
const messageContent = computed(() =>
@@ -66,10 +66,10 @@ const nonInlineAttachments = computed(() =>
)
const getFullName = computed(() => {
return convStore.conversation.data.first_name + ' ' + convStore.conversation.data.last_name
return convStore.current.first_name + ' ' + convStore.current.last_name
})
const avatarFallback = computed(() => {
return convStore.conversation.data.first_name.toUpperCase().substring(0, 2)
return convStore.current.first_name.toUpperCase().substring(0, 2)
})
</script>

View File

@@ -37,6 +37,7 @@ import { useConversationStore } from '@/stores/conversation'
import { Button } from '@/components/ui/button'
import { RefreshCw } from 'lucide-vue-next'
import { useEmitter } from '@/composables/useEmitter'
import { EMITTER_EVENTS } from '@/constants/emitterEvents'
const conversationStore = useConversationStore()
const threadEl = ref(null)
@@ -44,18 +45,19 @@ const emitter = useEmitter()
const scrollToBottom = () => {
setTimeout(() => {
console.log('scrolling..')
const thread = threadEl.value
if (thread) {
thread.scrollTop = thread.scrollHeight
}
}, 0)
}, 50)
}
onMounted(() => {
scrollToBottom()
// On new outgoing message to the current conversation, scroll to the bottom.
emitter.on('new-outgoing-message', (data) => {
if (data.conversation_uuid === conversationStore.conversation.data.uuid) {
emitter.on(EMITTER_EVENTS.NEW_OUTGOING_MESSAGE, (data) => {
if (data.conversation_uuid === conversationStore.current.uuid) {
scrollToBottom()
}
})
@@ -63,7 +65,7 @@ onMounted(() => {
// On conversation change scroll to the bottom
watch(
() => conversationStore.conversation.data,
() => conversationStore.current.uuid,
() => {
scrollToBottom()
}

View File

@@ -1,13 +1,5 @@
export const CONVERSATION_FILTERS = {
ALL: 'status_all',
STATUS_OPEN: 'status_open',
STATUS_PROCESSING: 'status_processing',
STATUS_SPAM: 'status_spam',
STATUS_RESOLVED: 'status_resolved'
}
export const CONVERSATION_LIST_TYPE = {
ASSIGNED: 'assigned',
UNASSIGNED: 'team',
UNASSIGNED: 'unassigned',
ALL: 'all'
}

View File

@@ -1,4 +1,6 @@
export const EMITTER_EVENTS = {
REFRESH_LIST: 'refresh-list',
SHOW_TOAST: 'show-toast'
SHOW_TOAST: 'show-toast',
NEW_OUTGOING_MESSAGE: 'new-outgoing-message',
NEW_INCOMING_MESSAGE: 'new-incoming-message',
}

View File

@@ -1,10 +1,10 @@
import { defineStore } from 'pinia'
import { computed, reactive, onUnmounted } from 'vue'
import { handleHTTPError } from '@/utils/http'
import { useToast } from '@/components/ui/toast/use-toast'
import { CONVERSATION_LIST_TYPE } from '@/constants/conversation'
import api from '@/api'
import { useEmitter } from '@/composables/useEmitter'
import { EMITTER_EVENTS } from '@/constants/emitterEvents'
import api from '@/api'
export const useConversationStore = defineStore('conversation', () => {
// List of conversations
@@ -36,20 +36,18 @@ export const useConversationStore = defineStore('conversation', () => {
// Map to track seen msg UUIDs for deduplication
let seenConversationUUIDs = new Map()
let seenMessageUUIDs = new Set()
let previousConvListType = ''
let previousFilter = ''
let previousConversationListType = ''
let reRenderInterval = setInterval(() => {
conversations.data = [...conversations.data]
}, 60000)
const { toast } = useToast()
}, 120000)
const emitter = useEmitter()
// Clear the reRenderInterval when the store is destroyed.
// Cleanup.
onUnmounted(() => {
clearInterval(reRenderInterval)
})
// Computed property to sort conversations by last_message_at
// Sort conversations by last_message_at
const sortedConversations = computed(() => {
if (!conversations.data) {
return []
@@ -59,7 +57,7 @@ export const useConversationStore = defineStore('conversation', () => {
)
})
// Computed property to sort messages by created_at
// Sort messages by created_at
const sortedMessages = computed(() => {
if (!messages.data) {
return []
@@ -67,6 +65,18 @@ export const useConversationStore = defineStore('conversation', () => {
return [...messages.data].sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
})
// Marks a conversation as read.
function markAsRead (uuid) {
const index = conversations.data.findIndex((conv) => conv.uuid === uuid)
if (index !== -1) {
conversations.data[index].unread_message_count = 0
}
}
const currentContactName = computed(() => {
return conversation.data?.first_name + " " + conversation.data?.last_name
})
const getContactFullName = (uuid) => {
if (conversations?.data) {
const conv = conversations.data.find((conv) => conv.uuid === uuid)
@@ -74,29 +84,31 @@ export const useConversationStore = defineStore('conversation', () => {
}
}
function markAsRead (uuid) {
const index = conversations.data.findIndex((conv) => conv.uuid === uuid)
if (index !== -1) {
conversations.data[index].unread_message_count = 0
}
}
const current = computed(() => {
return conversation.data
})
// Fetches conversation by uuid.
async function fetchConversation (uuid) {
conversation.loading = true
try {
const resp = await api.getConversation(uuid)
conversation.data = resp.data.data
// mark this conversation as read.
// Mark this conversation as read.
markAsRead(uuid)
// reset messages state on new conversation fetch.
resetMessages()
} catch (error) {
conversation.errorMessage = handleHTTPError(error).message
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Something went wrong',
variant: 'destructive',
description: conversation.errorMessage
})
} finally {
conversation.loading = false
}
}
// Fetches participants of conversation by uuid.
async function fetchParticipants (uuid) {
try {
const resp = await api.getConversationParticipants(uuid)
@@ -106,17 +118,24 @@ export const useConversationStore = defineStore('conversation', () => {
}, {})
updateParticipants(participants)
} catch (error) {
console.error('Error fetching participants:', error)
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Something went wrong',
variant: 'destructive',
description: handleHTTPError(error).message
})
}
}
// Fetches messages of a conversation.
async function fetchMessages (uuid) {
// Reset state.
resetMessages()
messages.loading = true
try {
const response = await api.getMessages(uuid, messages.page)
const response = await api.getConversationMessages(uuid, messages.page)
const fetchedMessages = response.data?.data || []
// Filter out messages that have already been seen
// Filter out messages already seen.
const newMessages = fetchedMessages.filter((message) => {
if (!seenMessageUUIDs.has(message.uuid)) {
seenMessageUUIDs.add(message.uuid)
@@ -134,11 +153,12 @@ export const useConversationStore = defineStore('conversation', () => {
messages.hasMore = false
}
// Add new messages to the messages data
// Add new messages to the messages state.
messages.data.unshift(...newMessages)
} catch (error) {
toast({
title: 'Could not fetch messages, Please try again.',
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Something went wrong',
variant: 'destructive',
description: handleHTTPError(error).message
})
@@ -149,14 +169,16 @@ export const useConversationStore = defineStore('conversation', () => {
}
}
// Fetches next page of messages by incrementing the page number.
async function fetchNextMessages () {
messages.page++
fetchMessages(conversation.data.uuid)
}
// Fetches a specific message of conversation
async function fetchMessage (cuuid, uuid) {
try {
const response = await api.getMessage(cuuid, uuid)
const response = await api.getConversationMessage(cuuid, uuid)
if (response?.data?.data) {
const message = response.data.data
if (!messages.data.some((m) => m.uuid === message.uuid)) {
@@ -165,45 +187,51 @@ export const useConversationStore = defineStore('conversation', () => {
}
} catch (error) {
messages.errorMessage = handleHTTPError(error).message
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Could not fetch message',
variant: 'destructive',
description: messages.errorMessage
})
}
}
function onFilterchange () {
conversations.data = null
// On conversation type(tab) change reset state.
function onConversationTypeChange () {
conversations.data = []
conversations.page = 1
conversations.hasMore = true
seenConversationUUIDs.clear()
}
async function fetchConversations (type, filter, showLoader = true) {
// fetchConversationsList fetches list of conversations based on type(tab).
async function fetchConversationsList (type, showLoader = true) {
conversations.errorMessage = ''
if (showLoader)
conversations.loading = true
conversations.errorMessage = ''
if (type !== previousConvListType || filter !== previousFilter) {
onFilterchange()
previousConvListType = type
previousFilter = filter
if (type !== previousConversationListType) {
onConversationTypeChange()
previousConversationListType = type
}
try {
let response
switch (type) {
case CONVERSATION_LIST_TYPE.ASSIGNED:
response = await api.getAssignedConversations(conversations.page, filter)
response = await api.getAssignedConversations(conversations.page)
break
case CONVERSATION_LIST_TYPE.UNASSIGNED:
response = await api.getTeamConversations(conversations.page, filter)
response = await api.getUnassignedConversations(conversations.page)
break
case CONVERSATION_LIST_TYPE.ALL:
response = await api.getAllConversations(conversations.page, filter)
response = await api.getAllConversations(conversations.page)
break
default:
console.warn(`Invalid type ${type}`)
return
}
// Merge new conversations if any
// Merge new conversations if any.
if (response?.data?.data) {
const newConversations = response.data.data.filter((conversation) => {
if (!seenConversationUUIDs.has(conversation.uuid)) {
@@ -217,9 +245,8 @@ export const useConversationStore = defineStore('conversation', () => {
conversations.data = []
}
if (newConversations.length === 0) {
if (newConversations.length === 0)
conversations.hasMore = false
}
conversations.data.push(...newConversations)
} else {
@@ -235,14 +262,18 @@ export const useConversationStore = defineStore('conversation', () => {
// Increments the page and fetches the next set of conversations.
function fetchNextConversations (type, filter) {
conversations.page++
fetchConversations(type, filter)
fetchConversationsList(type, filter)
}
async function updatePriority (v) {
try {
await api.updateConversationPriority(conversation.data.uuid, { priority: v })
} catch (error) {
// Pass.
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Something went wrong',
variant: 'destructive',
description: handleHTTPError(error).message
})
}
}
@@ -250,8 +281,8 @@ export const useConversationStore = defineStore('conversation', () => {
try {
await api.updateConversationStatus(conversation.data.uuid, { status: v })
} catch (error) {
toast({
title: 'Uh oh! Could not update status, Please try again.',
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Something went wrong',
variant: 'destructive',
description: handleHTTPError(error).message
})
@@ -262,8 +293,8 @@ export const useConversationStore = defineStore('conversation', () => {
try {
await api.upsertTags(conversation.data.uuid, v)
} catch (error) {
toast({
title: 'Uh oh! Could not add tags, Please try again.',
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Something went wrong',
variant: 'destructive',
description: handleHTTPError(error).message
})
@@ -274,7 +305,11 @@ export const useConversationStore = defineStore('conversation', () => {
try {
await api.updateAssignee(conversation.data.uuid, type, v)
} catch (error) {
// Pass.
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Something went wrong',
variant: 'destructive',
description: handleHTTPError(error).message
})
}
}
@@ -301,18 +336,18 @@ export const useConversationStore = defineStore('conversation', () => {
// Update the last message for a conversation.
function updateConversationLastMessage (message) {
const conv = conversations.data.find((c) => c.uuid === message.conversation_uuid)
if (conv) {
conv.last_message = message.content
conv.last_message_at = message.created_at
const listConversation = conversations.data.find((c) => c.uuid === message.conversation_uuid)
if (listConversation) {
listConversation.last_message = message.content
listConversation.last_message_at = message.created_at
// Increment unread count only if conversation is not open.
if (conv.uuid !== conversation?.data?.uuid) {
conv.unread_message_count += 1
if (listConversation.uuid !== conversation?.data?.uuid) {
listConversation.unread_message_count += 1
}
}
}
// Update message in a conversation.
// Adds a new message to conversation.
function updateConversationMessageList (message) {
// Fetch entire message only if the convesation is open and the message is not present in the list.
if (conversation?.data?.uuid === message.conversation_uuid) {
@@ -322,7 +357,12 @@ export const useConversationStore = defineStore('conversation', () => {
updateAssigneeLastSeen(message.conversation_uuid)
if (message.type === 'outgoing') {
setTimeout(() => {
emitter.emit('new-outgoing-message', { conversation_uuid: message.conversation_uuid })
emitter.emit(EMITTER_EVENTS.NEW_OUTGOING_MESSAGE, { conversation_uuid: message.conversation_uuid })
}, 50)
}
if (message.type === 'incoming') {
setTimeout(() => {
emitter.emit(EMITTER_EVENTS.NEW_INCOMING_MESSAGE, { conversation_uuid: message.conversation_uuid })
}, 50)
}
}
@@ -336,21 +376,19 @@ export const useConversationStore = defineStore('conversation', () => {
}
function updateMessageProp (message) {
// Update prop in list.
const existingMessage = messages.data.find((m) => m.uuid === message.uuid)
if (existingMessage) {
existingMessage[message.prop] = message.value
}
}
function updateConversationProp (conversation) {
// Update prop if conversation is open.
if (conversation?.data?.uuid === conversation.uuid) {
conversation.data[conversation.prop] = conversation.value
function updateConversationProp (update) {
// Update prop in open conversation.
if (conversation.data?.uuid === update.uuid) {
conversation.data[update.prop] = update.value
}
// Update prop in list.
const existingConversation = conversations?.data?.find((c) => c.uuid === conversation.uuid)
// Update prop in conversation list.
const existingConversation = conversations?.data?.find((c) => c.uuid === update.uuid)
if (existingConversation) {
existingConversation[conversation.prop] = conversation.value
}
@@ -389,6 +427,8 @@ export const useConversationStore = defineStore('conversation', () => {
messages,
sortedConversations,
sortedMessages,
current,
currentContactName,
conversationUUIDExists,
updateConversationProp,
addNewConversation,
@@ -400,7 +440,7 @@ export const useConversationStore = defineStore('conversation', () => {
updateAssigneeLastSeen,
updateConversationMessageList,
fetchConversation,
fetchConversations,
fetchConversationsList,
fetchMessages,
upsertTags,
updateAssignee,

View File

@@ -1,11 +1,11 @@
<template>
<ResizablePanelGroup direction="horizontal" auto-save-id="conversation.vue.resizable.panel">
<ResizablePanel :min-size="23" :default-size="23" :max-size="40">
<ResizablePanel :min-size="23" :default-size="23" :max-size="40" class="shadow-md shadow-gray-300">
<ConversationList></ConversationList>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel>
<Conversation v-if="conversationStore.conversation.data"></Conversation>
<Conversation v-if="conversationStore.current"></Conversation>
<ConversationPlaceholder v-else></ConversationPlaceholder>
</ResizablePanel>
<ResizableHandle />
@@ -13,7 +13,8 @@
:min-size="10"
:default-size="16"
:max-size="30"
v-if="conversationStore.conversation.data"
v-if="conversationStore.current"
class="shadow shadow-gray-300"
>
<ConversationSideBar></ConversationSideBar>
</ResizablePanel>

View File

@@ -288,23 +288,23 @@ func (c *Manager) GetConversationUUID(id int) (string, error) {
return uuid, nil
}
// GetAllConversations retrieves all conversations with optional filtering, ordering, and pagination.
func (c *Manager) GetAllConversations(order, orderBy, filter string, page, pageSize int) ([]models.Conversation, error) {
return c.GetConversations(0, models.AllConversations, order, orderBy, filter, page, pageSize)
// GetAllConversationsList retrieves all conversations with optional filtering, ordering, and pagination.
func (c *Manager) GetAllConversationsList(order, orderBy string, page, pageSize int) ([]models.Conversation, error) {
return c.GetConversations(0, models.AllConversations, order, orderBy, page, pageSize)
}
// GetAssignedConversations retrieves conversations assigned to a specific user with optional filtering, ordering, and pagination.
func (c *Manager) GetAssignedConversations(userID int, order, orderBy, filter string, page, pageSize int) ([]models.Conversation, error) {
return c.GetConversations(userID, models.AssignedConversations, order, orderBy, filter, page, pageSize)
// GetAssignedConversationsList retrieves conversations assigned to a specific user with optional filtering, ordering, and pagination.
func (c *Manager) GetAssignedConversationsList(userID int, order, orderBy string, page, pageSize int) ([]models.Conversation, error) {
return c.GetConversations(userID, models.AssignedConversations, order, orderBy, page, pageSize)
}
// GetTeamConversations retrieves conversations assigned to a team the user is part of with optional filtering, ordering, and pagination.
func (c *Manager) GetTeamConversations(userID int, order, orderBy, filter string, page, pageSize int) ([]models.Conversation, error) {
return c.GetConversations(userID, models.AssigneeTypeTeam, order, orderBy, filter, page, pageSize)
// GetUnassignedConversationsList retrieves conversations assigned to a team the user is part of with optional filtering, ordering, and pagination.
func (c *Manager) GetUnassignedConversationsList(userID int, order, orderBy string, page, pageSize int) ([]models.Conversation, error) {
return c.GetConversations(userID, models.UnassignedConversations, order, orderBy, page, pageSize)
}
// GetConversations retrieves conversations based on user ID, type, and optional filtering, ordering, and pagination.
func (c *Manager) GetConversations(userID int, typ, order, orderBy, filter string, page, pageSize int) ([]models.Conversation, error) {
func (c *Manager) GetConversations(userID int, typ, order, orderBy string, page, pageSize int) ([]models.Conversation, error) {
var conversations = make([]models.Conversation, 0)
if orderBy == "" {
@@ -314,7 +314,7 @@ func (c *Manager) GetConversations(userID int, typ, order, orderBy, filter strin
order = "DESC"
}
query, qArgs, err := c.generateConversationsListQuery(userID, c.q.GetConversations, typ, order, orderBy, filter, page, pageSize)
query, qArgs, err := c.generateConversationsListQuery(userID, c.q.GetConversations, typ, order, orderBy, page, pageSize)
if err != nil {
c.lo.Error("error generating conversations query", "error", err)
return conversations, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil)
@@ -335,10 +335,10 @@ func (c *Manager) GetConversations(userID int, typ, order, orderBy, filter strin
}
// GetConversationUUIDs retrieves the UUIDs of conversations based on user ID, type, and optional filtering, ordering, and pagination.
func (c *Manager) GetConversationUUIDs(userID, page, pageSize int, typ, filter string) ([]string, error) {
func (c *Manager) GetConversationUUIDs(userID, page, pageSize int, typ string) ([]string, error) {
var ids = make([]string, 0)
query, qArgs, err := c.generateConversationsListQuery(userID, c.q.GetConversationsUUIDs, typ, "", "", filter, page, pageSize)
query, qArgs, err := c.generateConversationsListQuery(userID, c.q.GetConversationsUUIDs, typ, "", "", page, pageSize)
if err != nil {
c.lo.Error("error generating conversations query", "error", err)
return ids, err
@@ -465,6 +465,7 @@ func (c *Manager) UpdateConversationStatus(uuid string, status []byte, actor umo
if err := c.RecordStatusChange(statusStr, uuid, actor); err != nil {
return envelope.NewError(envelope.GeneralError, "Error recording status change", nil)
}
c.BroadcastConversationPropertyUpdate(uuid, "status", string(status))
return nil
}
@@ -555,7 +556,7 @@ func (t *Manager) UpsertConversationTags(uuid string, tagIDs []int) error {
}
// generateConversationsListQuery generates the SQL query to list conversations with optional filtering, ordering, and pagination.
func (c *Manager) generateConversationsListQuery(userID int, baseQuery, typ, order, orderBy, filter string, page, pageSize int) (string, []interface{}, error) {
func (c *Manager) generateConversationsListQuery(userID int, baseQuery, typ, order, orderBy string, page, pageSize int) (string, []interface{}, error) {
var (
qArgs []interface{}
cond string
@@ -566,7 +567,7 @@ func (c *Manager) generateConversationsListQuery(userID int, baseQuery, typ, ord
case models.AssignedConversations:
cond = "AND c.assigned_user_id = $1"
qArgs = append(qArgs, userID)
case models.TeamConversations:
case models.UnassignedConversations:
cond = "AND c.assigned_user_id IS NULL AND c.assigned_team_id IN (SELECT team_id FROM team_members WHERE user_id = $1)"
qArgs = append(qArgs, userID)
case models.AllConversations:
@@ -575,10 +576,6 @@ func (c *Manager) generateConversationsListQuery(userID int, baseQuery, typ, ord
return "", nil, fmt.Errorf("invalid conversation type %s", typ)
}
if filterClause, ok := models.ValidFilters[filter]; ok {
cond += " AND " + filterClause
}
// Ensure orderBy & order is valid.
var orderByClause = ""
if slices.Contains(models.ValidOrderBy, orderBy) {

View File

@@ -27,9 +27,9 @@ var (
AssigneeTypeTeam = "team"
AssigneeTypeUser = "user"
AllConversations = "all"
AssignedConversations = "assigned"
TeamConversations = "team"
AllConversations = "all"
AssignedConversations = "assigned"
UnassignedConversations = "unassigned"
)
type Conversation struct {

View File

@@ -118,7 +118,7 @@ func (c *Client) processIncomingMessage(data []byte) {
// Add the new subscriptions.
for page := 1; page <= maxConversationsPagesToSub; page++ {
conversationUUIDs, err := c.Hub.conversationStore.GetConversationUUIDs(c.ID, page, maxConversationsPageSize, subReq.Type, subReq.Filter)
conversationUUIDs, err := c.Hub.conversationStore.GetConversationUUIDs(c.ID, page, maxConversationsPageSize, subReq.Type)
if err != nil {
log.Println("error fetching conversation ids", err)
continue

View File

@@ -24,7 +24,7 @@ type Hub struct {
// ConversationStore defines the interface for retrieving conversation UUIDs.
type ConversationStore interface {
GetConversationUUIDs(userID, page, pageSize int, typ, predefinedFilter string) ([]string, error)
GetConversationUUIDs(userID, page, pageSize int, typ string) ([]string, error)
}
// NewHub creates a new Hub.

View File

@@ -123,10 +123,10 @@ CREATE TABLE roles (
-- Create roles.
INSERT INTO roles
(permissions, "name", description)
VALUES('{conversations:read_team,conversations:read_all,conversations:read,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,messages:read,messages:write,templates:write,templates:read,roles:delete,roles:write,roles:read,inboxes:delete,inboxes:write,inboxes:read,automations:write,automations:delete,automations:read,teams:write,teams:read,users:write,users:read,dashboard_global:read,canned_responses:delete,tags:delete,canned_responses:write,tags:write,status:delete,status:write,status:read,oidc:delete,oidc:read,oidc:write,settings_notifications:read,settings_notifications:write,settings_general:write,templates:delete,admin:read}', 'Admin', 'Role for users who have access to the admin panel.');
VALUES('{conversations:read_unassigned,conversations:read_all,conversations:read,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,messages:read,messages:write,templates:write,templates:read,roles:delete,roles:write,roles:read,inboxes:delete,inboxes:write,inboxes:read,automations:write,automations:delete,automations:read,teams:write,teams:read,users:write,users:read,dashboard_global:read,canned_responses:delete,tags:delete,canned_responses:write,tags:write,status:delete,status:write,status:read,oidc:delete,oidc:read,oidc:write,settings_notifications:read,settings_notifications:write,settings_general:write,templates:delete,admin:read}', 'Admin', 'Role for users who have access to the admin panel.');
INSERT INTO roles
(permissions, "name", description)
VALUES('{conversations:read,conversations:read_team,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,status:write,status:delete,tags:write,tags:delete,canned_responses:write,canned_responses:delete,dashboard:global,users:write,users:read,teams:read,teams:write,automations:read,automations:write,automations:delete,inboxes:read,inboxes:write,inboxes:delete,roles:read,roles:write,roles:delete,templates:read,templates:write,messages:read,messages:write,dashboard_global:read,oidc:delete,status:read,oidc:write,settings_notifications:read,oidc:read,settings_general:write,settings_notifications:write,conversations:read_all,templates:delete}', 'Agent', 'Role for all agents with limited access.');
VALUES('{conversations:read,conversations:read_unassigned,conversations:read_assigned,conversations:update_user_assignee,conversations:update_team_assignee,conversations:update_priority,conversations:update_status,conversations:update_tags,status:write,status:delete,tags:write,tags:delete,canned_responses:write,canned_responses:delete,dashboard:global,users:write,users:read,teams:read,teams:write,automations:read,automations:write,automations:delete,inboxes:read,inboxes:write,inboxes:delete,roles:read,roles:write,roles:delete,templates:read,templates:write,messages:read,messages:write,dashboard_global:read,oidc:delete,status:read,oidc:write,settings_notifications:read,oidc:read,settings_general:write,settings_notifications:write,conversations:read_all,templates:delete}', 'Agent', 'Role for all agents with limited access.');
DROP TABLE IF EXISTS settings CASCADE;
CREATE TABLE settings (
@@ -285,3 +285,27 @@ VALUES
('notification.email.email_address', '""'::jsonb),
('notification.email.max_msg_retries', '3'::jsonb),
('notification.email.enabled', 'false'::jsonb);
INSERT INTO priority
(id, "name")
VALUES(1, 'Low');
INSERT INTO priority
(id, "name")
VALUES(2, 'Medium');
INSERT INTO priority
(id, "name")
VALUES(3, 'High');
INSERT INTO status
(id, "name")
VALUES(1, 'Open');
INSERT INTO status
(id, "name")
VALUES(2, 'Replied');
INSERT INTO status
(id, "name")
VALUES(3, 'Resolved');
INSERT INTO status
(id, "name")
VALUES(4, 'Closed');