From 18514cd02199d257f1fdf1f78b9cd46bbd560f7c Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 28 Aug 2026 01:42:24 +0530 Subject: [PATCH 01/32] search and paginate agents, teams, tags and macros instead of loading them all at boot --- cmd/handlers.go | 45 +- cmd/macro.go | 84 +++- cmd/macro_test.go | 55 +++ cmd/main.go | 2 +- cmd/pagination_test.go | 131 ++++++ cmd/tags.go | 15 +- cmd/teams.go | 15 +- cmd/users.go | 24 +- frontend/apps/main/src/App.vue | 3 - frontend/apps/main/src/api/index.js | 12 +- .../combobox/SelectAgentCombobox.vue | 94 ++++ .../components/combobox/SelectCombobox.vue | 10 + .../components/combobox/SelectTagCombobox.vue | 73 ++++ .../combobox/SelectTeamCombobox.vue | 76 ++++ .../src/components/filter/ConnectorToggle.vue | 35 +- .../src/components/filter/FilterBuilder.vue | 35 +- .../components/filter/FilterGroupBuilder.vue | 10 +- .../src/components/filter/FilterGroupCard.vue | 19 +- .../main/src/components/filter/FilterRow.vue | 57 ++- .../src/components/filter/filterTree.test.js | 405 ++++++++++++++++++ .../main/src/components/sidebar/Sidebar.vue | 2 +- .../src/composables/useActivityLogFilters.js | 4 +- .../src/composables/useConversationFilters.js | 32 +- .../admin/activity-log/ActivityLog.vue | 6 +- .../features/admin/automation/ActionBox.vue | 55 ++- .../src/features/admin/automation/RuleBox.vue | 32 +- .../admin/help-center/ArticleEditSheet.vue | 41 +- .../features/admin/macros/ActionBuilder.vue | 27 +- .../src/features/admin/macros/MacroForm.vue | 89 ++-- .../admin/macros/dataTableDropdown.vue | 6 +- .../admin/shared-views/SharedViewForm.vue | 20 +- .../main/src/features/admin/sla/SLAForm.vue | 20 +- .../main/src/features/command/CommandBox.vue | 193 ++++++--- .../conversation/CreateConversation.vue | 27 +- .../features/conversation/ReplyBoxContent.vue | 108 ++--- .../features/conversation/ReplyBoxMenuBar.vue | 87 ++-- .../list/ConversationBulkActionToolbar.vue | 51 +-- .../sidebar/ConversationSideBar.vue | 50 +-- .../apps/main/src/features/view/ViewForm.vue | 9 +- frontend/apps/main/src/stores/conversation.js | 25 +- frontend/apps/main/src/stores/macro.js | 88 ++-- frontend/apps/main/src/stores/macro.test.js | 135 ++++++ frontend/apps/main/src/stores/tag.js | 42 +- frontend/apps/main/src/stores/team.js | 42 +- frontend/apps/main/src/stores/users.js | 46 +- frontend/apps/main/src/utils/remote-lookup.js | 91 ++++ .../apps/main/src/utils/remote-lookup.test.js | 120 ++++++ .../admin/automations/CreateOrEditRule.vue | 9 +- .../src/views/admin/macros/CreateMacro.vue | 2 +- .../main/src/views/admin/macros/EditMacro.vue | 3 +- .../main/src/views/admin/macros/MacroList.vue | 10 +- .../conversation/ConversationDetailView.vue | 4 +- frontend/cypress/e2e/api/lookupSearch.cy.js | 217 ++++++++++ .../cypress/e2e/ui/commandPaletteMacros.cy.js | 114 +++++ .../cypress/e2e/ui/lateLookupValues.cy.js | 273 ++++++++++++ frontend/cypress/e2e/ui/macroForm.cy.js | 6 +- frontend/cypress/e2e/ui/sharedViewForm.cy.js | 349 +++++++++++++++ .../components/ui/combobox/ComboBox.vue | 43 +- .../components/ui/command/CommandDialog.vue | 18 +- .../components/ui/command/CommandInput.vue | 15 +- .../components/ui/dialog/DialogContent.vue | 8 +- .../components/ui/select/SelectTag.vue | 74 +++- internal/conversation/conversation.go | 17 +- internal/conversation/message.go | 3 +- internal/conversation/queries.sql | 4 +- internal/dbutil/dbutil.go | 8 + internal/macro/macro.go | 24 ++ internal/macro/macro_test.go | 174 ++++++++ internal/macro/models/models.go | 23 + internal/macro/queries.sql | 50 +++ internal/tag/queries.sql | 16 + internal/tag/tag.go | 26 +- internal/tag/tag_test.go | 105 +++++ internal/team/queries.sql | 5 +- internal/team/team.go | 40 +- internal/team/team_test.go | 139 ++++++ internal/testutil/testutil.go | 19 + internal/user/agent.go | 20 +- internal/user/agent_test.go | 120 ++++++ 79 files changed, 3903 insertions(+), 683 deletions(-) create mode 100644 cmd/macro_test.go create mode 100644 cmd/pagination_test.go create mode 100644 frontend/apps/main/src/components/combobox/SelectAgentCombobox.vue create mode 100644 frontend/apps/main/src/components/combobox/SelectTagCombobox.vue create mode 100644 frontend/apps/main/src/components/combobox/SelectTeamCombobox.vue create mode 100644 frontend/apps/main/src/components/filter/filterTree.test.js create mode 100644 frontend/apps/main/src/stores/macro.test.js create mode 100644 frontend/apps/main/src/utils/remote-lookup.js create mode 100644 frontend/apps/main/src/utils/remote-lookup.test.js create mode 100644 frontend/cypress/e2e/api/lookupSearch.cy.js create mode 100644 frontend/cypress/e2e/ui/commandPaletteMacros.cy.js create mode 100644 frontend/cypress/e2e/ui/lateLookupValues.cy.js create mode 100644 frontend/cypress/e2e/ui/sharedViewForm.cy.js create mode 100644 internal/macro/macro_test.go create mode 100644 internal/tag/tag_test.go create mode 100644 internal/team/team_test.go create mode 100644 internal/user/agent_test.go diff --git a/cmd/handlers.go b/cmd/handlers.go index 56d5cf17..607df0b0 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -14,7 +14,10 @@ import ( "github.com/zerodha/fastglue" ) -const maxPageSize = 500 +const ( + maxPageSize = 500 + maxIDsParam = 200 +) // initHandlers initializes the HTTP routes and handlers for the application. func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { @@ -117,7 +120,9 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { // Macros. g.GET("/api/v1/macros", auth(handleGetMacros)) - g.GET("/api/v1/macros/{id}", perm(handleGetMacro, "macros:manage")) + g.GET("/api/v1/macros/compact", perm(handleGetMacrosCompact, "macros:manage")) + g.GET("/api/v1/macros/search", auth(handleSearchMacros)) + g.GET("/api/v1/macros/{id}", auth(handleGetMacro)) g.POST("/api/v1/macros", perm(handleCreateMacro, "macros:manage")) g.PUT("/api/v1/macros/{id}", perm(handleUpdateMacro, "macros:manage")) g.DELETE("/api/v1/macros/{id}", perm(handleDeleteMacro, "macros:manage")) @@ -545,6 +550,26 @@ func serveWidgetJS(r *fastglue.Request) error { return nil } +// getIDsParam parses a comma separated list of positive IDs from a query param. +func getIDsParam(r *fastglue.Request, name string) []int { + raw := strings.TrimSpace(string(r.RequestCtx.QueryArgs().Peek(name))) + if raw == "" { + return nil + } + var ids []int + for part := range strings.SplitSeq(raw, ",") { + id, err := strconv.Atoi(strings.TrimSpace(part)) + if err != nil || id <= 0 { + continue + } + ids = append(ids, id) + if len(ids) == maxIDsParam { + break + } + } + return ids +} + // getPagination extracts page and page_size from query params with defaults. func getPagination(r *fastglue.Request) (page, pageSize int) { page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page"))) @@ -561,6 +586,22 @@ func getPagination(r *fastglue.Request) (page, pageSize int) { return page, pageSize } +// getOptionalPagination reads page/page_size, returning pageSize 0 (fetch everything) when absent. +func getOptionalPagination(r *fastglue.Request) (page, pageSize int) { + page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page"))) + pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size"))) + if page < 1 { + page = 1 + } + if pageSize < 0 { + pageSize = 0 + } + if pageSize > maxPageSize { + pageSize = maxPageSize + } + return page, pageSize +} + // sendErrorEnvelope sends a standardized error response to the client. func sendErrorEnvelope(r *fastglue.Request, err error) error { e, ok := err.(envelope.Error) diff --git a/cmd/macro.go b/cmd/macro.go index 38895c72..d8856f49 100644 --- a/cmd/macro.go +++ b/cmd/macro.go @@ -21,17 +21,58 @@ func handleGetMacros(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } for i, m := range macros { - var actions []autoModels.RuleAction - if err := json.Unmarshal(m.Actions, &actions); err != nil { - app.lo.Error("error unmarshalling macro actions", "macro_id", m.ID, "error", err) + if macros[i].Actions, err = decorateMacroActions(app, m.Actions); err != nil { + app.lo.Error("error decorating macro actions", "macro_id", m.ID, "error", err) return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError) } - // Set display values for actions as the value field can contain DB IDs - if err := setDisplayValues(app, actions); err != nil { - app.lo.Warn("error setting display values", "error", err) + } + return r.SendEnvelope(macros) +} + +// handleGetMacrosCompact returns macros without message content, all of them without page params. +func handleGetMacrosCompact(r *fastglue.Request) error { + var ( + app = r.Context.(*App) + query = string(r.RequestCtx.QueryArgs().Peek("q")) + ) + page, pageSize := getOptionalPagination(r) + macros, err := app.macro.GetAllCompact(query, page, pageSize) + if err != nil { + return sendErrorEnvelope(r, err) + } + for i, m := range macros { + if macros[i].Actions, err = decorateMacroActions(app, m.Actions); err != nil { + app.lo.Error("error decorating macro actions", "macro_id", m.ID, "error", err) + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError) } - if macros[i].Actions, err = json.Marshal(actions); err != nil { - app.lo.Error("error marshalling macro actions", "macro_id", m.ID, "error", err) + } + return r.SendEnvelope(macros) +} + +// handleSearchMacros returns the top macros without message content visible to the requesting agent. +func handleSearchMacros(r *fastglue.Request) error { + var ( + app = r.Context.(*App) + auser = r.RequestCtx.UserValue("user").(amodels.User) + query = string(r.RequestCtx.QueryArgs().Peek("q")) + view = string(r.RequestCtx.QueryArgs().Peek("view")) + ) + switch view { + case "", models.VisibleWhenReplying, models.VisibleWhenStartingConversation, models.VisibleWhenAddingPrivateNote: + default: + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError) + } + user, err := app.user.GetAgentCachedOrLoad(auser.ID) + if err != nil { + return sendErrorEnvelope(r, err) + } + macros, err := app.macro.SearchCompact(query, view, user.ID, user.Teams.IDs()) + if err != nil { + return sendErrorEnvelope(r, err) + } + for i, m := range macros { + if macros[i].Actions, err = decorateMacroActions(app, m.Actions); err != nil { + app.lo.Error("error decorating macro actions", "macro_id", m.ID, "error", err) return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError) } } @@ -53,17 +94,8 @@ func handleGetMacro(r *fastglue.Request) error { return sendErrorEnvelope(r, err) } - var actions []autoModels.RuleAction - if err := json.Unmarshal(macro.Actions, &actions); err != nil { - app.lo.Error("error unmarshalling macro actions", "macro_id", id, "error", err) - return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError) - } - // Set display values for actions as the value field can contain DB IDs - if err := setDisplayValues(app, actions); err != nil { - app.lo.Warn("error setting display values", "error", err) - } - if macro.Actions, err = json.Marshal(actions); err != nil { - app.lo.Error("error marshalling macro actions", "macro_id", id, "error", err) + if macro.Actions, err = decorateMacroActions(app, macro.Actions); err != nil { + app.lo.Error("error decorating macro actions", "macro_id", id, "error", err) return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError) } @@ -226,6 +258,18 @@ func hasActionPermission(action string, userPerms []string) bool { return slices.Contains(userPerms, requiredPerm) } +// decorateMacroActions resolves display values for action IDs and re-marshals the actions. +func decorateMacroActions(app *App, raw json.RawMessage) (json.RawMessage, error) { + var actions []autoModels.RuleAction + if err := json.Unmarshal(raw, &actions); err != nil { + return nil, err + } + if err := setDisplayValues(app, actions); err != nil { + app.lo.Warn("error setting display values", "error", err) + } + return json.Marshal(actions) +} + // setDisplayValues sets display values for actions. func setDisplayValues(app *App, actions []autoModels.RuleAction) error { getters := map[string]func(int) (string, error){ @@ -238,7 +282,7 @@ func setDisplayValues(app *App, actions []autoModels.RuleAction) error { return t.Name, nil }, autoModels.ActionAssignUser: func(id int) (string, error) { - u, err := app.user.GetAgent(id, "") + u, err := app.user.GetAgentCachedOrLoad(id) if err != nil { app.lo.Warn("user not found for macro action", "user_id", id) return "", err diff --git a/cmd/macro_test.go b/cmd/macro_test.go new file mode 100644 index 00000000..b619a0f2 --- /dev/null +++ b/cmd/macro_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "encoding/json" + "testing" + + "github.com/abhinavxd/libredesk/internal/macro/models" + "github.com/abhinavxd/libredesk/internal/testutil" +) + +// Both list endpoints marshal these structs directly, their JSON shape is the API contract. +func TestMacroJSONShapes(t *testing.T) { + full := testutil.JSONKeys(t, models.Macro{Actions: json.RawMessage(`[]`)}) + for _, key := range []string{"id", "name", "actions", "visibility", "visible_when", "message_content", "user_id", "team_id", "usage_count", "created_at", "updated_at"} { + if !full[key] { + t.Errorf("Macro JSON is missing %q", key) + } + } + if full["has_message_content"] { + t.Error("Macro JSON must not have has_message_content") + } + + compact := testutil.JSONKeys(t, models.MacroCompact{Actions: json.RawMessage(`[]`)}) + for _, key := range []string{"id", "name", "actions", "visibility", "visible_when", "has_message_content", "user_id", "team_id", "usage_count", "created_at", "updated_at"} { + if !compact[key] { + t.Errorf("MacroCompact JSON is missing %q", key) + } + } + if compact["message_content"] { + t.Error("MacroCompact JSON must not have message_content") + } +} + +func TestDecorateMacroActions(t *testing.T) { + app := newValidatorTestApp(t) + + out, err := decorateMacroActions(app, json.RawMessage(`[{"type":"add_tags","value":["urgent"]}]`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + var actions []map[string]any + if err := json.Unmarshal(out, &actions); err != nil { + t.Fatalf("output is not valid JSON: %v", err) + } + if len(actions) != 1 { + t.Fatalf("got %d actions, want 1", len(actions)) + } + if _, ok := actions[0]["display_value"]; !ok { + t.Error("decorated action is missing display_value") + } + + if _, err := decorateMacroActions(app, json.RawMessage(`{not json`)); err == nil { + t.Error("expected an error for malformed actions JSON") + } +} diff --git a/cmd/main.go b/cmd/main.go index 43636c87..41db7ef6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -340,7 +340,7 @@ func main() { g.Router.NotFound = helpCenterHostNotFound(app, g) // Buffers above this are dropped rather than reused, and the ones we keep stay with the connection until it closes. - fasthttp.SetBodySizePoolLimit(64<<10, 1<<20) // request: 64 KiB, response: 1 MiB + fasthttp.SetBodySizePoolLimit(64<<10, 128<<10) // request: 64 KiB, response: 128 KiB s := &fasthttp.Server{ Name: appName, diff --git a/cmd/pagination_test.go b/cmd/pagination_test.go new file mode 100644 index 00000000..8376d684 --- /dev/null +++ b/cmd/pagination_test.go @@ -0,0 +1,131 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/abhinavxd/libredesk/internal/dbutil" + "github.com/valyala/fasthttp" + "github.com/zerodha/fastglue" +) + +func requestWithQuery(query string) *fastglue.Request { + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/api/v1/thing?" + query) + return &fastglue.Request{RequestCtx: ctx} +} + +func TestGetIDsParam(t *testing.T) { + tests := []struct { + query string + want []int + }{ + {"", nil}, + {"ids=", nil}, + {"ids=%20%20", nil}, + {"ids=5", []int{5}}, + {"ids=5,7,9", []int{5, 7, 9}}, + {"ids=%205%20,%207%20", []int{5, 7}}, + {"ids=5,5,5", []int{5, 5, 5}}, + {"ids=abc", nil}, + {"ids=0", nil}, + {"ids=-3", nil}, + {"ids=1,abc,2", []int{1, 2}}, + {"ids=1,,2", []int{1, 2}}, + {"ids=1,0,-2,3", []int{1, 3}}, + {"ids=9223372036854775808", nil}, + {"other=1", nil}, + } + + for _, tc := range tests { + got := getIDsParam(requestWithQuery(tc.query), "ids") + if len(got) != len(tc.want) { + t.Fatalf("query %q = %v, want %v", tc.query, got, tc.want) + } + for i := range got { + if got[i] != tc.want[i] { + t.Fatalf("query %q = %v, want %v", tc.query, got, tc.want) + } + } + } +} + +func TestGetIDsParamCap(t *testing.T) { + query := "ids=1" + for i := 2; i <= maxIDsParam+50; i++ { + query += fmt.Sprintf(",%d", i) + } + + got := getIDsParam(requestWithQuery(query), "ids") + + if len(got) != maxIDsParam { + t.Fatalf("got %d ids, want the cap of %d", len(got), maxIDsParam) + } + if got[0] != 1 || got[maxIDsParam-1] != maxIDsParam { + t.Fatalf("cap dropped the wrong end: first %d, last %d", got[0], got[maxIDsParam-1]) + } +} + +func TestGetPaginationDefaultsAndClamps(t *testing.T) { + tests := []struct { + query string + wantPage int + wantPageSize int + }{ + {"", 1, 30}, + {"page=0&page_size=0", 1, 30}, + {"page=-4&page_size=-9", 1, 30}, + {"page=abc&page_size=abc", 1, 30}, + {"page=3&page_size=50", 3, 50}, + {"page_size=100000", 1, maxPageSize}, + } + + for _, tc := range tests { + page, pageSize := getPagination(requestWithQuery(tc.query)) + if page != tc.wantPage || pageSize != tc.wantPageSize { + t.Fatalf("query %q = page %d size %d, want page %d size %d", tc.query, page, pageSize, tc.wantPage, tc.wantPageSize) + } + } +} + +func TestGetOptionalPaginationFetchesEverythingWithoutParams(t *testing.T) { + tests := []struct { + query string + wantPage int + wantPageSize int + }{ + {"", 1, 0}, + {"page=2", 2, 0}, + {"page_size=0", 1, 0}, + {"page_size=-5", 1, 0}, + {"page=2&page_size=50", 2, 50}, + {"page_size=100000", 1, maxPageSize}, + } + + for _, tc := range tests { + page, pageSize := getOptionalPagination(requestWithQuery(tc.query)) + if page != tc.wantPage || pageSize != tc.wantPageSize { + t.Fatalf("query %q = page %d size %d, want page %d size %d", tc.query, page, pageSize, tc.wantPage, tc.wantPageSize) + } + } +} + +func TestPageOffset(t *testing.T) { + tests := []struct { + page int + pageSize int + want int + }{ + {1, 30, 0}, + {2, 30, 30}, + {3, 50, 100}, + {1, 0, 0}, + {5, 0, 0}, + } + + for _, tc := range tests { + if got := dbutil.PageOffset(tc.page, tc.pageSize); got != tc.want { + t.Fatalf("PageOffset(%d, %d) = %d, want %d", tc.page, tc.pageSize, got, tc.want) + } + } +} diff --git a/cmd/tags.go b/cmd/tags.go index a40b1255..61e62eaf 100644 --- a/cmd/tags.go +++ b/cmd/tags.go @@ -9,12 +9,21 @@ import ( "github.com/zerodha/fastglue" ) -// handleGetTags returns all tags from the database. +// handleGetTags returns tags from the database, all of them without page params. func handleGetTags(r *fastglue.Request) error { var ( - app = r.Context.(*App) + app = r.Context.(*App) + query = string(r.RequestCtx.QueryArgs().Peek("q")) ) - t, err := app.tag.GetAll() + if ids := getIDsParam(r, "ids"); len(ids) > 0 { + t, err := app.tag.GetByIDs(ids) + if err != nil { + return sendErrorEnvelope(r, err) + } + return r.SendEnvelope(t) + } + page, pageSize := getOptionalPagination(r) + t, err := app.tag.GetAll(query, page, pageSize) if err != nil { return sendErrorEnvelope(r, err) } diff --git a/cmd/teams.go b/cmd/teams.go index 4727f2eb..bf7a8d5e 100644 --- a/cmd/teams.go +++ b/cmd/teams.go @@ -21,12 +21,21 @@ func handleGetTeams(r *fastglue.Request) error { return r.SendEnvelope(teams) } -// handleGetTeamsCompact returns a list of all teams in a compact format. +// handleGetTeamsCompact returns teams in a compact format, all of them without page params. func handleGetTeamsCompact(r *fastglue.Request) error { var ( - app = r.Context.(*App) + app = r.Context.(*App) + query = string(r.RequestCtx.QueryArgs().Peek("q")) ) - teams, err := app.team.GetAllCompact() + if ids := getIDsParam(r, "ids"); len(ids) > 0 { + teams, err := app.team.GetAllCompactByIDs(ids) + if err != nil { + return sendErrorEnvelope(r, err) + } + return r.SendEnvelope(teams) + } + page, pageSize := getOptionalPagination(r) + teams, err := app.team.GetAllCompact(query, page, pageSize) if err != nil { return sendErrorEnvelope(r, err) } diff --git a/cmd/users.go b/cmd/users.go index 8278d403..6e201b9c 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -65,10 +65,28 @@ func handleGetAgents(r *fastglue.Request) error { return r.SendEnvelope(agents) } -// handleGetAgentsCompact returns all agents in a compact format. +// handleGetAgentsCompact returns agents in a compact format, all of them without page params. func handleGetAgentsCompact(r *fastglue.Request) error { - var app = r.Context.(*App) - agents, err := app.user.GetAgentsCompact() + var ( + app = r.Context.(*App) + query = string(r.RequestCtx.QueryArgs().Peek("q")) + userType = string(r.RequestCtx.QueryArgs().Peek("type")) + enabledOnly = string(r.RequestCtx.QueryArgs().Peek("enabled")) == "true" + ) + switch userType { + case "", models.UserTypeAgent, models.UserTypeAIAssistant: + default: + return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError) + } + if ids := getIDsParam(r, "ids"); len(ids) > 0 { + agents, err := app.user.GetAgentsCompactByIDs(ids) + if err != nil { + return sendErrorEnvelope(r, err) + } + return r.SendEnvelope(agents) + } + page, pageSize := getOptionalPagination(r) + agents, err := app.user.GetAgentsCompact(query, userType, enabledOnly, page, pageSize) if err != nil { return sendErrorEnvelope(r, err) } diff --git a/frontend/apps/main/src/App.vue b/frontend/apps/main/src/App.vue index 90436594..f69e5bab 100644 --- a/frontend/apps/main/src/App.vue +++ b/frontend/apps/main/src/App.vue @@ -81,7 +81,6 @@ import { useInboxStore } from './stores/inbox' import { useUsersStore } from './stores/users' import { useTeamStore } from './stores/team' import { useSlaStore } from './stores/sla' -import { useMacroStore } from './stores/macro' import { useSharedViewStore } from './stores/sharedView' import { useTagStore } from './stores/tag' import { useCustomAttributeStore } from './stores/customAttributes' @@ -136,7 +135,6 @@ const usersStore = useUsersStore() const teamStore = useTeamStore() const inboxStore = useInboxStore() const slaStore = useSlaStore() -const macroStore = useMacroStore() const sharedViewStore = useSharedViewStore() const tagStore = useTagStore() const customAttributeStore = useCustomAttributeStore() @@ -187,7 +185,6 @@ const initStores = async () => { teamStore.fetchTeams(), inboxStore.fetchInboxes(), slaStore.fetchSlas(), - macroStore.loadMacros(), tagStore.fetchTags(), customAttributeStore.fetchCustomAttributes() ]) diff --git a/frontend/apps/main/src/api/index.js b/frontend/apps/main/src/api/index.js index 71d22f28..7361e1b2 100644 --- a/frontend/apps/main/src/api/index.js +++ b/frontend/apps/main/src/api/index.js @@ -229,7 +229,7 @@ const createTeam = (data) => http.post('/api/v1/teams', data, { 'Content-Type': 'application/json' } }) -const getTeamsCompact = () => http.get('/api/v1/teams/compact') +const getTeamsCompact = (params) => http.get('/api/v1/teams/compact', { params }) const deleteTeam = (id) => http.delete(`/api/v1/teams/${id}`) const updateUser = (id, data) => http.put(`/api/v1/agents/${id}`, data, { @@ -238,7 +238,7 @@ const updateUser = (id, data) => } }) const getUsers = () => http.get('/api/v1/agents') -const getUsersCompact = () => http.get('/api/v1/agents/compact') +const getUsersCompact = (params) => http.get('/api/v1/agents/compact', { params }) const updateCurrentUser = (data) => http.put('/api/v1/agents/me', data, { headers: { @@ -278,7 +278,7 @@ const createUser = (data) => 'Content-Type': 'application/json' } }) -const getTags = () => http.get('/api/v1/tags') +const getTags = (params) => http.get('/api/v1/tags', { params }) const importTags = (data) => http.post('/api/v1/tags/import', data, { headers: { @@ -349,7 +349,8 @@ const getConversation = (uuid) => http.get(`/api/v1/conversations/${uuid}`, { ab const getConversationTranscript = (uuid) => http.get(`/api/v1/conversations/${uuid}/transcript`, { responseType: 'blob' }) const getContactPageVisits = (uuid) => http.get(`/api/v1/conversations/${uuid}/page-visits`, { abortOnRoute: true }) -const getAllMacros = () => http.get('/api/v1/macros') +const getMacrosCompact = (params) => http.get('/api/v1/macros/compact', { params }) +const searchMacros = (params) => http.get('/api/v1/macros/search', { params }) const getMacro = (id) => http.get(`/api/v1/macros/${id}`) const createMacro = (data) => http.post('/api/v1/macros', data, { @@ -665,7 +666,8 @@ export default { getConversationTranscript, getCurrentUser, getCurrentUserTeams, - getAllMacros, + getMacrosCompact, + searchMacros, getMacro, createMacro, updateMacro, diff --git a/frontend/apps/main/src/components/combobox/SelectAgentCombobox.vue b/frontend/apps/main/src/components/combobox/SelectAgentCombobox.vue new file mode 100644 index 00000000..d0a060f3 --- /dev/null +++ b/frontend/apps/main/src/components/combobox/SelectAgentCombobox.vue @@ -0,0 +1,94 @@ + + + diff --git a/frontend/apps/main/src/components/combobox/SelectCombobox.vue b/frontend/apps/main/src/components/combobox/SelectCombobox.vue index 3546ff4d..0602011b 100644 --- a/frontend/apps/main/src/components/combobox/SelectCombobox.vue +++ b/frontend/apps/main/src/components/combobox/SelectCombobox.vue @@ -6,6 +6,8 @@ :items="items" :placeholder="placeholder" :align="align" + :search="search" + :searching="searching" >