diff --git a/cmd/conversation.go b/cmd/conversation.go index 8a17f404..b21298e6 100644 --- a/cmd/conversation.go +++ b/cmd/conversation.go @@ -264,6 +264,39 @@ func handleGetSidebarCounts(r *fastglue.Request) error { return r.SendEnvelope(counts) } +// handleGetViewCount returns the sidebar badge count for one view. +func handleGetViewCount(r *fastglue.Request) error { + var ( + app = r.Context.(*App) + auser = r.RequestCtx.UserValue("user").(amodels.User) + viewID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) + ) + if viewID < 1 { + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError) + } + + view, err := app.view.Get(viewID) + if err != nil { + return sendErrorEnvelope(r, err) + } + + user, err := app.user.GetAgentCachedOrLoad(auser.ID) + if err != nil { + return sendErrorEnvelope(r, err) + } + + if !conversation.UserCanAccessView(view, auser.ID, user.Teams.IDs()) { + return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("conversation.viewPermissionDenied"), nil, envelope.PermissionError) + } + + count, err := app.conversation.GetViewCount(user.ID, user.Permissions, user.Teams.IDs(), view) + if err != nil { + return sendErrorEnvelope(r, err) + } + + return r.SendEnvelope(map[string]int{"count": count}) +} + // handleGetTeamUnassignedConversations returns conversations assigned to a team but not to any user. func handleGetTeamUnassignedConversations(r *fastglue.Request) error { var ( diff --git a/cmd/handlers.go b/cmd/handlers.go index fdce720e..9d38c928 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -59,6 +59,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.GET("/api/v1/conversations/sidebar-counts", perm(handleGetSidebarCounts, "conversations:read")) g.GET("/api/v1/teams/{id}/conversations/unassigned", perm(handleGetTeamUnassignedConversations, "conversations:read_team_inbox")) g.GET("/api/v1/views/{id}/conversations", perm(handleGetViewConversations, "conversations:read")) + g.GET("/api/v1/views/{id}/count", perm(handleGetViewCount, "conversations:read")) g.GET("/api/v1/conversations/{uuid}", perm(handleGetConversation, "conversations:read")) g.GET("/api/v1/conversations/{uuid}/participants", perm(handleGetConversationParticipants, "conversations:read")) g.PUT("/api/v1/conversations/{uuid}/assignee/user", perm(handleUpdateUserAssignee, "conversations:update_user_assignee")) diff --git a/frontend/apps/main/src/App.vue b/frontend/apps/main/src/App.vue index 1beca153..fd899254 100644 --- a/frontend/apps/main/src/App.vue +++ b/frontend/apps/main/src/App.vue @@ -261,7 +261,8 @@ const refreshViews = async (data) => { // TODO: move model to constants. if (data?.model === 'view') { await getUserViews() - conversationStore.fetchSidebarCounts({ force: true }) + if (data.id) conversationStore.fetchViewCount(data.id) + else conversationStore.fetchSidebarCounts({ force: true }) const openID = route.params.viewID // If the open view was edited its filters may have changed, refetch. if (openID && userViews.value.some((v) => String(v.id) === String(openID))) { diff --git a/frontend/apps/main/src/api/index.js b/frontend/apps/main/src/api/index.js index 475d1307..d9370210 100644 --- a/frontend/apps/main/src/api/index.js +++ b/frontend/apps/main/src/api/index.js @@ -382,6 +382,7 @@ const getAllConversations = (params) => const getMentionedConversations = (params) => http.get('/api/v1/conversations/mentioned', { params, abortOnRoute: true }) const getSidebarCounts = () => http.get('/api/v1/conversations/sidebar-counts') +const getViewCount = (id) => http.get(`/api/v1/views/${id}/count`) const getViewConversations = (id, params) => http.get(`/api/v1/views/${id}/conversations`, { params, abortOnRoute: true }) const uploadMedia = (data) => @@ -655,6 +656,7 @@ export default { getAllConversations, getMentionedConversations, getSidebarCounts, + getViewCount, getTeamUnassignedConversations, getViewConversations, getOverviewCharts, diff --git a/frontend/apps/main/src/features/view/ViewForm.vue b/frontend/apps/main/src/features/view/ViewForm.vue index 5c91681e..c7e099dd 100644 --- a/frontend/apps/main/src/features/view/ViewForm.vue +++ b/frontend/apps/main/src/features/view/ViewForm.vue @@ -163,18 +163,17 @@ const onSubmit = form.handleSubmit(async (values) => { try { const payload = { ...values, filters: serializeFilterTree(values.filters) } - if (payload.id) { - await api.updateView(payload.id, payload) - emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { - description: t('globals.messages.savedSuccessfully') - }) + let viewID = payload.id + if (viewID) { + await api.updateView(viewID, payload) } else { - await api.createView(payload) - emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { - description: t('globals.messages.savedSuccessfully') - }) + const resp = await api.createView(payload) + viewID = resp?.data?.data?.id } - emitter.emit(EMITTER_EVENTS.REFRESH_LIST, { model: 'view' }) + emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { + description: t('globals.messages.savedSuccessfully') + }) + emitter.emit(EMITTER_EVENTS.REFRESH_LIST, { model: 'view', id: viewID }) openDialog.value = false form.resetForm() } catch (error) { diff --git a/frontend/apps/main/src/stores/conversation.js b/frontend/apps/main/src/stores/conversation.js index b08c12ee..b47418e4 100644 --- a/frontend/apps/main/src/stores/conversation.js +++ b/frontend/apps/main/src/stores/conversation.js @@ -86,6 +86,15 @@ export const useConversationStore = defineStore('conversation', () => { return sidebarCountsRequest } + async function fetchViewCount (viewID) { + try { + const resp = await api.getViewCount(viewID) + sidebarCounts.views[viewID] = resp?.data?.data?.count || 0 + } catch { + // The sidebar works without counts. + } + } + // WS events burst one per conversation; leading + trailing keeps it to two requests per burst. const SIDEBAR_COUNTS_EVENT_THROTTLE = 45_000 const refreshSidebarCounts = useThrottleFn( @@ -1312,6 +1321,7 @@ export const useConversationStore = defineStore('conversation', () => { isSelected, sidebarCounts, fetchSidebarCounts, + fetchViewCount, refreshSidebarCounts } }) diff --git a/internal/conversation/sidebar_counts.go b/internal/conversation/sidebar_counts.go index d2be4a96..657be04f 100644 --- a/internal/conversation/sidebar_counts.go +++ b/internal/conversation/sidebar_counts.go @@ -53,6 +53,22 @@ func (c *Manager) GetSidebarCounts(viewingUserID int, permissions []string, team return out, nil } +// GetViewCount returns the capped open count for one view. +func (c *Manager) GetViewCount(viewingUserID int, permissions []string, teamIDs []int, view vmodels.View) (int, error) { + lists := ListsForUserPermissions(permissions) + if len(lists) == 0 { + return 0, nil + } + ctx, cancel := context.WithTimeout(context.Background(), sidebarCountsQueryTimeout) + defer cancel() + + counts, err := c.getViewCounts(ctx, viewingUserID, teamIDs, lists, []vmodels.View{view}) + if err != nil { + return 0, err + } + return counts[view.ID], nil +} + func (c *Manager) fillStandardSidebarCounts(ctx context.Context, out *models.SidebarCounts, userID int, permissions []string) error { var row struct { Assigned int `db:"assigned"`