mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-18 18:18:23 -05:00
Add Default Agent Support and promote Agents to be part of suite (#35091)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
* Add default agent support and App Bar integration - Add Agents section to App Bar, separating it from Core Products. - Implement Default Agent logic in AtMentionProvider: - Promote default agent to top of suggestions for empty '@' prefix. - Filter duplicate agent entry from main list when default is shown. - Add `AgentTag` component for UI distinction. - Update `mattermost-plugin-ai` and `server/public` dependencies. - Add unit tests for default agent suggestion logic. * Add missing files (server deps, types) * Fix pre-commit check failures - Fix TypeScript errors in test files: - Add missing displayName property to defaultAgent in at_mention_provider test - Add missing fetchAgents mock in textbox test - Fix Go assignment mismatch in integration_action_test.go (CreatePostAsUser returns 3 values) - Fix license copyright year in plugins/mattermost-ai/assets/embed.go - Update i18n translations (add tag.default.agent) - Regenerate Go serialized files, mmctl docs, and update go.mod/go.sum * Update snapshot tests for textbox and at_mention_suggestion * Undo mmctl docs changes * Undo more changes * revert package-lock.json * Update dep for ai plugin * Update again * Update at_mention_provider to filter out agent duplicates and add .cursor/ to gitignore - Filter agent usernames from priorityProfiles and localAndRemoteMembers to prevent duplicate entries in autocomplete suggestions - Add .cursor/ directory to .gitignore Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e4bd8398ab
commit
4269ebf913
23 changed files with 346 additions and 37 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -163,3 +163,4 @@ docker-compose.override.yaml
|
|||
**/CLAUDE.local.md
|
||||
**/CLAUDE.md
|
||||
.cursorrules
|
||||
.cursor/
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ require (
|
|||
github.com/mattermost/gosaml2 v0.10.0
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956
|
||||
github.com/mattermost/logr/v2 v2.0.22
|
||||
github.com/mattermost/mattermost-plugin-ai v1.5.0
|
||||
github.com/mattermost/mattermost/server/public v0.1.20
|
||||
github.com/mattermost/mattermost-plugin-ai v1.8.1
|
||||
github.com/mattermost/mattermost/server/public v0.1.22-0.20251105210629-8bf4a00724e2
|
||||
github.com/mattermost/morph v1.1.0
|
||||
github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0
|
||||
github.com/mattermost/squirrel v0.5.0
|
||||
|
|
@ -131,7 +131,7 @@ require (
|
|||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/jsonschema-go v0.2.3 // indirect
|
||||
github.com/google/jsonschema-go v0.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
|
|
|
|||
|
|
@ -267,8 +267,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
|||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/jsonschema-go v0.2.3 h1:dkP3B96OtZKKFvdrUSaDkL+YDx8Uw9uC4Y+eukpCnmM=
|
||||
github.com/google/jsonschema-go v0.2.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
||||
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
|
|
@ -406,10 +406,14 @@ github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb
|
|||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI=
|
||||
github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r/lP4=
|
||||
github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.5.0 h1:64P8CadbrglgiQMiYqE9kZngrvIb5Ze7Jv+iK832RbI=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.5.0/go.mod h1:sgR9+nLFCjYSE9vlqxLZxHZ+6Kz2NJw9Qko+ywVX2k0=
|
||||
github.com/mattermost/mattermost/server/public v0.1.20 h1:N39ZOyYvCVZXABetpb0tVrm/08Od3q7d/D45QZjJ63s=
|
||||
github.com/mattermost/mattermost/server/public v0.1.20/go.mod h1:ZSdLCHIFYDTmX5FNCJ7LoI2CLZ1rQqZcfJ3ZF81QadY=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.7.3-0.20260128145904-ae362cf86c17 h1:Mz9ZbcFPF3OItbDtor/FGJRHNfi7QFnPESjgCmL876A=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.7.3-0.20260128145904-ae362cf86c17/go.mod h1:giiKPJpaLV4qv/odeGMyO7ltkgaWZzsbUA92Wi7EMkA=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.8.1-0.20260205142725-644bf6ed9297 h1:oojqLsPxMAY7tMOG6cQZ0BAfiYZvGMarRi3dq7krzpU=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.8.1-0.20260205142725-644bf6ed9297/go.mod h1:Uco4K7ypsrZWcD256ezvgZDqolJfHYiExN2lYiHmVDo=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.8.1 h1:qymxDayy3vJPhm59XA8q0oLR8uobPgx0SOB7IMG9ZMM=
|
||||
github.com/mattermost/mattermost-plugin-ai v1.8.1/go.mod h1:Uco4K7ypsrZWcD256ezvgZDqolJfHYiExN2lYiHmVDo=
|
||||
github.com/mattermost/mattermost/server/public v0.1.22-0.20251105210629-8bf4a00724e2 h1:RJtCnj9nF/wb0Fb+O0qAPgUoWP5CTTDnHzHD5ciGlJ8=
|
||||
github.com/mattermost/mattermost/server/public v0.1.22-0.20251105210629-8bf4a00724e2/go.mod h1:X0RG3lk0XK0SFSH67JS/xporlz3TxItHEPlFIrsQIa8=
|
||||
github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw=
|
||||
github.com/mattermost/morph v1.1.0/go.mod h1:gD+EaqX2UMyyuzmF4PFh4r33XneQ8Nzi+0E8nXjMa3A=
|
||||
github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f h1:tAXeRJSWo6EK7wDq1TcxMHIxHRyjrE62ihvsigdg4Q0=
|
||||
|
|
@ -542,13 +546,8 @@ github.com/reflog/dateconstraints v0.2.1 h1:Hz1n2Q1vEm0Rj5gciDQcCN1iPBwfFjxUJy32
|
|||
github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.5 h1:kNlmACZuwC8ZWPLoJtD+HtZOsKJgYn7gXgUIcRB7dbo=
|
||||
github.com/richardlehane/msoleps v1.0.5/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ require (
|
|||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
|
|
@ -59,7 +58,6 @@ require (
|
|||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wiggin77/merror v1.0.5 // indirect
|
||||
github.com/wiggin77/srslog v1.0.1 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
|
|||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
|
||||
github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw=
|
||||
github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk=
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ exports[`components/TextBox should match snapshot with additional, optional prop
|
|||
"channelId": "channelId",
|
||||
"currentUserId": "currentUserId",
|
||||
"data": null,
|
||||
"defaultAgent": undefined,
|
||||
"disableDispatches": false,
|
||||
"forceDispatch": false,
|
||||
"getProfilesInChannel": [Function],
|
||||
|
|
@ -242,6 +243,7 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
|
|||
"channelId": "channelId",
|
||||
"currentUserId": "currentUserId",
|
||||
"data": null,
|
||||
"defaultAgent": undefined,
|
||||
"disableDispatches": false,
|
||||
"forceDispatch": false,
|
||||
"getProfilesInChannel": [Function],
|
||||
|
|
@ -384,6 +386,7 @@ exports[`components/TextBox should throw error when new property is too long 1`]
|
|||
"channelId": "channelId",
|
||||
"currentUserId": "currentUserId",
|
||||
"data": null,
|
||||
"defaultAgent": undefined,
|
||||
"disableDispatches": false,
|
||||
"forceDispatch": false,
|
||||
"getProfilesInChannel": [Function],
|
||||
|
|
@ -526,6 +529,7 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
|
|||
"channelId": "channelId",
|
||||
"currentUserId": "currentUserId",
|
||||
"data": null,
|
||||
"defaultAgent": undefined,
|
||||
"disableDispatches": false,
|
||||
"forceDispatch": false,
|
||||
"getProfilesInChannel": [Function],
|
||||
|
|
|
|||
|
|
@ -43,16 +43,27 @@ export default function AppBar() {
|
|||
}
|
||||
|
||||
const coreProductsPluginIds = [suitePluginIds.focalboard, suitePluginIds.playbooks];
|
||||
const agentsPluginId = suitePluginIds.agents;
|
||||
|
||||
// Partition app bar components: Playbooks/Boards vs other plugins
|
||||
const [coreProductComponents, pluginComponents] = partition(appBarPluginComponents, ({pluginId}) => {
|
||||
return coreProductsPluginIds.includes(pluginId);
|
||||
});
|
||||
|
||||
// Partition channel header components: Agents vs others
|
||||
const [agentsComponents, otherChannelHeaderComponents] = partition(channelHeaderComponents, ({pluginId}) => {
|
||||
return pluginId === agentsPluginId;
|
||||
});
|
||||
|
||||
const items = [
|
||||
...agentsComponents,
|
||||
...coreProductComponents,
|
||||
getDivider(coreProductComponents.length, (pluginComponents.length + channelHeaderComponents.length + appBarBindings.length)),
|
||||
getDivider(
|
||||
agentsComponents.length + coreProductComponents.length,
|
||||
pluginComponents.length + otherChannelHeaderComponents.length + appBarBindings.length,
|
||||
),
|
||||
...pluginComponents,
|
||||
...channelHeaderComponents,
|
||||
...otherChannelHeaderComponents,
|
||||
...appBarBindings,
|
||||
].map((x) => {
|
||||
if (!x) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ exports[`at mention suggestion should display nick name of non signed in user 1`
|
|||
<SuggestionContainer
|
||||
aria-describedby="test-suggestion-1-description test-suggestion-1-youElement test-suggestion-1-status test-suggestion-1-botTag test-suggestion-1-sharedIcon test-suggestion-1-guestTag test-suggestion-1-groupMembers"
|
||||
aria-labelledby="test-suggestion-1-atMention"
|
||||
className=""
|
||||
data-testid="mentionSuggestion_user2"
|
||||
id="test-suggestion-1"
|
||||
isSelection={false}
|
||||
|
|
@ -130,6 +131,7 @@ exports[`at mention suggestion should not display nick name of the signed in use
|
|||
<SuggestionContainer
|
||||
aria-describedby="test-suggestion-1-description test-suggestion-1-youElement test-suggestion-1-status test-suggestion-1-botTag test-suggestion-1-sharedIcon test-suggestion-1-guestTag test-suggestion-1-groupMembers"
|
||||
aria-labelledby="test-suggestion-1-atMention"
|
||||
className=""
|
||||
data-testid="mentionSuggestion_user"
|
||||
id="test-suggestion-1"
|
||||
isSelection={false}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AtMentionProvider, {groupsGroup, membersGroup, nonMembersGroup, otherMembersGroup, specialMentionsGroup, type Props} from 'components/suggestion/at_mention_provider/at_mention_provider';
|
||||
import AtMentionProvider, {groupsGroup, membersGroup, nonMembersGroup, otherMembersGroup, specialMentionsGroup, defaultAgentGroup, type Props} from 'components/suggestion/at_mention_provider/at_mention_provider';
|
||||
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
|
|
@ -30,6 +30,7 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
|
|||
useChannelMentions: true,
|
||||
searchAssociatedGroupsForReference: jest.fn().mockResolvedValue(false),
|
||||
priorityProfiles: [],
|
||||
defaultAgent: undefined,
|
||||
};
|
||||
|
||||
it('should ignore pretexts that are not at-mentions', () => {
|
||||
|
|
@ -145,6 +146,164 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should suggest default agent for "@" when configured', async () => {
|
||||
const pretext = '@';
|
||||
const matchedPretext = '@';
|
||||
const agentUser = TestHelper.getUserMock({id: 'agentId', username: 'ai-agent', first_name: 'AI', last_name: 'Agent', is_bot: true});
|
||||
const defaultAgent = {
|
||||
id: 'agentId',
|
||||
displayName: 'AI Agent',
|
||||
username: 'ai-agent',
|
||||
service_id: 'serviceId',
|
||||
service_type: 'type',
|
||||
is_default: true,
|
||||
};
|
||||
|
||||
const itemsCall = [
|
||||
defaultAgentGroup({...agentUser, isAgent: true, isDefaultAgent: true}),
|
||||
membersGroup([
|
||||
userid10,
|
||||
userid3,
|
||||
userid1,
|
||||
userid2,
|
||||
userid4,
|
||||
]),
|
||||
groupsGroup([
|
||||
groupid1,
|
||||
groupid2,
|
||||
groupid3,
|
||||
]),
|
||||
specialMentionsGroup([
|
||||
{username: 'here'},
|
||||
{username: 'channel'},
|
||||
{username: 'all'},
|
||||
]),
|
||||
nonMembersGroup([
|
||||
userid5,
|
||||
userid6,
|
||||
]),
|
||||
];
|
||||
|
||||
const params = {
|
||||
...baseParams,
|
||||
defaultAgent,
|
||||
autocompleteUsersInChannel: jest.fn().mockImplementation(() => new Promise((resolve) => {
|
||||
resolve({data: {
|
||||
users: [userid4],
|
||||
out_of_channel: [userid5, userid6],
|
||||
agents: [agentUser],
|
||||
}});
|
||||
})),
|
||||
searchAssociatedGroupsForReference: jest.fn().mockImplementation(() => new Promise((resolve) => {
|
||||
resolve({
|
||||
data: [groupid1, groupid2, groupid3],
|
||||
});
|
||||
provider.updateMatches(resultCallback, itemsCall, matchedPretext);
|
||||
})),
|
||||
};
|
||||
|
||||
const provider = new AtMentionProvider(params);
|
||||
jest.spyOn(provider, 'getProfilesWithLastViewAtInChannel').mockImplementation(() => [userid10, userid3, userid1, userid2]);
|
||||
|
||||
const resultCallback = jest.fn();
|
||||
expect(provider.handlePretextChanged(pretext, resultCallback)).toEqual(true);
|
||||
|
||||
await Promise.resolve().then(() => {
|
||||
expect(resultCallback).toHaveBeenLastCalledWith({
|
||||
matchedPretext,
|
||||
groups: itemsCall,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep pinned default agent out of members and agents when agent is also a member or priority profile', async () => {
|
||||
const pretext = '@';
|
||||
const defaultAgentUser = TestHelper.getUserMock({id: 'defaultAgentId', username: 'default-agent', first_name: 'Default', last_name: 'Agent', is_bot: true});
|
||||
const otherAgentUser = TestHelper.getUserMock({id: 'otherAgentId', username: 'other-agent', first_name: 'Other', last_name: 'Agent', is_bot: true});
|
||||
|
||||
const defaultAgent = {
|
||||
id: 'defaultAgentId',
|
||||
displayName: 'Default Agent',
|
||||
username: 'default-agent',
|
||||
service_id: 'serviceId',
|
||||
service_type: 'type',
|
||||
is_default: true,
|
||||
};
|
||||
|
||||
const params = {
|
||||
...baseParams,
|
||||
defaultAgent,
|
||||
priorityProfiles: [defaultAgentUser],
|
||||
autocompleteUsersInChannel: jest.fn().mockResolvedValue({data: {
|
||||
users: [defaultAgentUser, userid4],
|
||||
out_of_channel: [userid5, userid6],
|
||||
agents: [defaultAgentUser, otherAgentUser],
|
||||
}}),
|
||||
searchAssociatedGroupsForReference: jest.fn().mockResolvedValue({
|
||||
data: [groupid1, groupid2, groupid3],
|
||||
}),
|
||||
};
|
||||
|
||||
const provider = new AtMentionProvider(params);
|
||||
jest.spyOn(provider, 'getProfilesWithLastViewAtInChannel').mockImplementation(() => [defaultAgentUser, userid10, userid3, userid1, userid2]);
|
||||
|
||||
const resultCallback = jest.fn();
|
||||
expect(provider.handlePretextChanged(pretext, resultCallback)).toEqual(true);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
const lastResult = resultCallback.mock.lastCall?.[0];
|
||||
expect(lastResult).toBeDefined();
|
||||
|
||||
const groups = lastResult.groups;
|
||||
expect(groups[0].key).toBe('defaultAgent');
|
||||
|
||||
const pinnedDefaultAgent = groups.find((group: {key: string}) => group.key === 'defaultAgent');
|
||||
const members = groups.find((group: {key: string}) => group.key === 'members');
|
||||
const agents = groups.find((group: {key: string}) => group.key === 'agents');
|
||||
|
||||
expect(pinnedDefaultAgent?.items[0].username).toBe(defaultAgentUser.username);
|
||||
expect(members?.items.map((item: {username: string}) => item.username)).not.toContain(defaultAgentUser.username);
|
||||
expect(agents?.items.map((item: {username: string}) => item.username)).not.toContain(defaultAgentUser.username);
|
||||
});
|
||||
|
||||
it('should keep non-default agents out of channel members when they are also in channel', async () => {
|
||||
const pretext = '@';
|
||||
const agentUser = TestHelper.getUserMock({id: 'agentId', username: 'member-agent', first_name: 'Member', last_name: 'Agent', is_bot: true});
|
||||
|
||||
const params = {
|
||||
...baseParams,
|
||||
autocompleteUsersInChannel: jest.fn().mockResolvedValue({data: {
|
||||
users: [agentUser, userid4],
|
||||
out_of_channel: [userid5, userid6],
|
||||
agents: [agentUser],
|
||||
}}),
|
||||
searchAssociatedGroupsForReference: jest.fn().mockResolvedValue({
|
||||
data: [groupid1, groupid2, groupid3],
|
||||
}),
|
||||
};
|
||||
|
||||
const provider = new AtMentionProvider(params);
|
||||
jest.spyOn(provider, 'getProfilesWithLastViewAtInChannel').mockImplementation(() => [agentUser, userid10, userid3, userid1, userid2]);
|
||||
|
||||
const resultCallback = jest.fn();
|
||||
expect(provider.handlePretextChanged(pretext, resultCallback)).toEqual(true);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
const lastResult = resultCallback.mock.lastCall?.[0];
|
||||
expect(lastResult).toBeDefined();
|
||||
|
||||
const groups = lastResult.groups;
|
||||
const members = groups.find((group: {key: string}) => group.key === 'members');
|
||||
const agents = groups.find((group: {key: string}) => group.key === 'agents');
|
||||
|
||||
expect(agents?.items.map((item: {username: string}) => item.username)).toContain(agentUser.username);
|
||||
expect(members?.items.map((item: {username: string}) => item.username)).not.toContain(agentUser.username);
|
||||
});
|
||||
|
||||
it('should have priorityProfiles at the top', async () => {
|
||||
const userid11 = TestHelper.getUserMock({id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'});
|
||||
const userid12 = TestHelper.getUserMock({id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import React from 'react';
|
|||
import {defineMessage} from 'react-intl';
|
||||
|
||||
import {CreationOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Agent} from '@mattermost/types/agents';
|
||||
import type {Group} from '@mattermost/types/groups';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
|
|
@ -34,6 +35,8 @@ type UserProfileWithLastViewAt = UserProfile & {last_viewed_at?: number};
|
|||
type CreatedProfile = UserProfile & {
|
||||
isCurrentUser?: boolean;
|
||||
last_viewed_at?: number;
|
||||
isAgent?: boolean;
|
||||
isDefaultAgent?: boolean;
|
||||
};
|
||||
|
||||
type SpecialMention = {
|
||||
|
|
@ -46,10 +49,19 @@ export type Props = {
|
|||
autocompleteUsersInChannel: (prefix: string) => Promise<ActionResult>;
|
||||
useChannelMentions: boolean;
|
||||
autocompleteGroups: Group[] | null;
|
||||
searchAssociatedGroupsForReference: (prefix: string) => Promise<{data: any}>;
|
||||
searchAssociatedGroupsForReference: (prefix: string) => Promise<{data: Group[]}>;
|
||||
priorityProfiles: UserProfile[] | undefined;
|
||||
defaultAgent?: Agent;
|
||||
}
|
||||
|
||||
// Data structure returned by autocomplete API
|
||||
type AutocompleteData = {
|
||||
users?: UserProfileWithLastViewAt[];
|
||||
groups?: Group[];
|
||||
out_of_channel?: UserProfileWithLastViewAt[];
|
||||
agents?: UserProfileWithLastViewAt[];
|
||||
};
|
||||
|
||||
// The AtMentionProvider provides matches for at mentions, including @here, @channel, @all,
|
||||
// users in the channel and users not in the channel. It mixes together results from the local
|
||||
// store with results fetched from the server.
|
||||
|
|
@ -59,10 +71,11 @@ export default class AtMentionProvider extends Provider {
|
|||
public autocompleteUsersInChannel: (prefix: string) => Promise<ActionResult>;
|
||||
public useChannelMentions: boolean;
|
||||
public autocompleteGroups: Group[] | null;
|
||||
public searchAssociatedGroupsForReference: (prefix: string) => Promise<{data: any}>;
|
||||
public searchAssociatedGroupsForReference: (prefix: string) => Promise<{data: Group[]}>;
|
||||
public priorityProfiles: UserProfile[] | undefined;
|
||||
public defaultAgent?: Agent;
|
||||
|
||||
public data: any;
|
||||
public data: AutocompleteData | null;
|
||||
public lastCompletedWord: string;
|
||||
public lastPrefixWithNoResults: string;
|
||||
public triggerCharacter: string = '@';
|
||||
|
|
@ -72,7 +85,7 @@ export default class AtMentionProvider extends Provider {
|
|||
constructor(props: Props) {
|
||||
super();
|
||||
|
||||
const {currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles} = props;
|
||||
const {currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles, defaultAgent} = props;
|
||||
|
||||
this.currentUserId = currentUserId;
|
||||
this.channelId = channelId;
|
||||
|
|
@ -81,6 +94,7 @@ export default class AtMentionProvider extends Provider {
|
|||
this.autocompleteGroups = autocompleteGroups;
|
||||
this.searchAssociatedGroupsForReference = searchAssociatedGroupsForReference;
|
||||
this.priorityProfiles = priorityProfiles;
|
||||
this.defaultAgent = defaultAgent;
|
||||
|
||||
this.data = null;
|
||||
this.lastCompletedWord = '';
|
||||
|
|
@ -90,7 +104,7 @@ export default class AtMentionProvider extends Provider {
|
|||
this.addLastViewAtToProfiles = makeAddLastViewAtToProfiles();
|
||||
}
|
||||
|
||||
setProps({currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles}: Props) {
|
||||
setProps({currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles, defaultAgent}: Props) {
|
||||
this.currentUserId = currentUserId;
|
||||
this.channelId = channelId;
|
||||
this.autocompleteUsersInChannel = autocompleteUsersInChannel;
|
||||
|
|
@ -98,6 +112,7 @@ export default class AtMentionProvider extends Provider {
|
|||
this.autocompleteGroups = autocompleteGroups;
|
||||
this.searchAssociatedGroupsForReference = searchAssociatedGroupsForReference;
|
||||
this.priorityProfiles = priorityProfiles;
|
||||
this.defaultAgent = defaultAgent;
|
||||
}
|
||||
|
||||
// specialMentions matches one of @here, @channel or @all, unless using /msg.
|
||||
|
|
@ -315,10 +330,18 @@ export default class AtMentionProvider extends Provider {
|
|||
const agentUsers = this.data.agents as UserProfileWithLastViewAt[];
|
||||
agents = agentUsers.
|
||||
filter((user: UserProfileWithLastViewAt) => this.filterProfile(user)).
|
||||
map((user: UserProfileWithLastViewAt) => this.createFromProfile(user)).
|
||||
map((user: UserProfileWithLastViewAt) => ({...this.createFromProfile(user), isAgent: true})).
|
||||
sort(orderUsers);
|
||||
}
|
||||
|
||||
const agentUsernames = new Set(agents.map((agent) => agent.username));
|
||||
let filteredPriorityProfiles = priorityProfiles;
|
||||
let filteredLocalAndRemoteMembers = localAndRemoteMembers;
|
||||
if (this.data?.agents && agentUsernames.size > 0) {
|
||||
filteredPriorityProfiles = priorityProfiles.filter((member) => !agentUsernames.has(member.username));
|
||||
filteredLocalAndRemoteMembers = localAndRemoteMembers.filter((member) => !agentUsernames.has(member.username));
|
||||
}
|
||||
|
||||
// handle groups
|
||||
const localGroups = this.localGroups();
|
||||
|
||||
|
|
@ -355,11 +378,26 @@ export default class AtMentionProvider extends Provider {
|
|||
|
||||
const items = [];
|
||||
|
||||
if (priorityProfiles.length > 0 || localAndRemoteMembers.length > 0) {
|
||||
items.push(membersGroup([...priorityProfiles, ...localAndRemoteMembers]));
|
||||
const shouldShowDefaultAgentAtTop = this.latestPrefix === '';
|
||||
if (shouldShowDefaultAgentAtTop && this.defaultAgent) {
|
||||
const defaultAgentUser = this.findAgentUser(this.defaultAgent);
|
||||
if (defaultAgentUser) {
|
||||
items.push(defaultAgentGroup({...this.createFromProfile(defaultAgentUser), isAgent: true, isDefaultAgent: true}));
|
||||
}
|
||||
}
|
||||
if (agents.length > 0) {
|
||||
items.push(agentsGroup(agents));
|
||||
|
||||
if (filteredPriorityProfiles.length > 0 || filteredLocalAndRemoteMembers.length > 0) {
|
||||
items.push(membersGroup([...filteredPriorityProfiles, ...filteredLocalAndRemoteMembers]));
|
||||
}
|
||||
|
||||
// Filter out default agent from agents group when shown at top to avoid duplicate entries
|
||||
let agentsToShow = agents;
|
||||
if (shouldShowDefaultAgentAtTop && this.defaultAgent) {
|
||||
agentsToShow = agents.filter((agent) => agent.username !== this.defaultAgent?.username);
|
||||
}
|
||||
|
||||
if (agentsToShow.length > 0) {
|
||||
items.push(agentsGroup(agentsToShow));
|
||||
}
|
||||
if (localAndRemoteGroups.length > 0) {
|
||||
items.push(groupsGroup(localAndRemoteGroups));
|
||||
|
|
@ -457,6 +495,15 @@ export default class AtMentionProvider extends Provider {
|
|||
|
||||
return profile;
|
||||
}
|
||||
|
||||
findAgentUser(agent: Agent): UserProfileWithLastViewAt | undefined {
|
||||
if (!this.data?.agents || !Array.isArray(this.data.agents)) {
|
||||
return undefined;
|
||||
}
|
||||
return (this.data.agents as UserProfileWithLastViewAt[]).find(
|
||||
(user) => user.username === agent.username,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function membersGroup(items: CreatedProfile[]) {
|
||||
|
|
@ -469,6 +516,15 @@ export function membersGroup(items: CreatedProfile[]) {
|
|||
};
|
||||
}
|
||||
|
||||
export function defaultAgentGroup(item: CreatedProfile) {
|
||||
return {
|
||||
key: 'defaultAgent',
|
||||
items: [item],
|
||||
terms: ['@' + item.username],
|
||||
component: AtMentionSuggestion,
|
||||
};
|
||||
}
|
||||
|
||||
export function agentsGroup(items: CreatedProfile[]) {
|
||||
return {
|
||||
key: 'agents',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
|
@ -13,6 +14,7 @@ import usePrefixedIds, {joinIds} from 'components/common/hooks/usePrefixedIds';
|
|||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import SharedUserIndicator from 'components/shared_user_indicator';
|
||||
import StatusIcon from 'components/status_icon';
|
||||
import AgentTag from 'components/widgets/tag/agent_tag';
|
||||
import BotTag from 'components/widgets/tag/bot_tag';
|
||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||
import Tag from 'components/widgets/tag/tag';
|
||||
|
|
@ -25,6 +27,8 @@ import type {SuggestionProps} from '../suggestion';
|
|||
|
||||
export interface Item extends UserProfile {
|
||||
isCurrentUser: boolean;
|
||||
isAgent?: boolean;
|
||||
isDefaultAgent?: boolean;
|
||||
}
|
||||
|
||||
interface Group extends Item {
|
||||
|
|
@ -199,6 +203,7 @@ const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item
|
|||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={classNames({'suggestion-list__item--default-agent': item.isDefaultAgent})}
|
||||
aria-labelledby={ids.atMention}
|
||||
aria-describedby={joinIds(ids.description, ids.youElement, ids.status, ids.botTag, ids.sharedIcon, ids.guestTag, ids.groupMembers)}
|
||||
data-testid={`mentionSuggestion_${itemname}`}
|
||||
|
|
@ -211,7 +216,7 @@ const AtMentionSuggestion = React.forwardRef<HTMLLIElement, SuggestionProps<Item
|
|||
>
|
||||
{'@' + itemname}
|
||||
</span>
|
||||
{item.is_bot && <span id={ids.botTag}><BotTag/></span>}
|
||||
{item.is_bot && <span id={ids.botTag}>{item.isAgent ? <AgentTag/> : <BotTag/>}</span>}
|
||||
{description && <span id={ids.description}>{description}</span>}
|
||||
{youElement}
|
||||
{customStatus}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ const SuggestionContainer = React.forwardRef<HTMLLIElement, SuggestionProps<unkn
|
|||
onMouseMove,
|
||||
|
||||
tabIndex = -1,
|
||||
className,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
|
|
@ -50,7 +51,7 @@ const SuggestionContainer = React.forwardRef<HTMLLIElement, SuggestionProps<unkn
|
|||
return (
|
||||
<li
|
||||
ref={ref}
|
||||
className={classNames('suggestion-list__item', {'suggestion--selected': isSelection})}
|
||||
className={classNames('suggestion-list__item', {'suggestion--selected': isSelection}, className)}
|
||||
role='option'
|
||||
onClick={handleClick}
|
||||
onMouseMove={handleMouseMove}
|
||||
|
|
|
|||
|
|
@ -212,10 +212,21 @@ function GroupedSuggestionsGroup({
|
|||
}: {
|
||||
children: React.ReactNode;
|
||||
groupKey: string | undefined;
|
||||
labelMessage: MessageDescriptor | string;
|
||||
labelMessage?: MessageDescriptor | string;
|
||||
}) {
|
||||
const labelId = `suggestionListGroup-${groupKey}`;
|
||||
|
||||
// If no label is provided, render the group without a header
|
||||
if (!labelMessage) {
|
||||
return (
|
||||
<ul
|
||||
role='group'
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul
|
||||
role='group'
|
||||
|
|
|
|||
|
|
@ -26,9 +26,10 @@ export type SuggestionResultsGroup<Item = unknown> = {
|
|||
key: string;
|
||||
|
||||
/**
|
||||
* The label for the group displayed to the user
|
||||
* The label for the group displayed to the user.
|
||||
* If omitted, the group will be rendered without a header.
|
||||
*/
|
||||
label: MessageDescriptor;
|
||||
label?: MessageDescriptor;
|
||||
|
||||
/**
|
||||
* A list of strings which the previously typed text may be replaced by.
|
||||
|
|
@ -197,7 +198,7 @@ export type ProviderResultsGrouped<Item = unknown> = {
|
|||
|
||||
export type ProviderResultsGroup<Item = unknown> = {
|
||||
key: string;
|
||||
label: MessageDescriptor;
|
||||
label?: MessageDescriptor;
|
||||
|
||||
terms: string[];
|
||||
items: Array<Item | Loading>;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ describe('components/TextBox', () => {
|
|||
autocompleteUsersInChannel: jest.fn(),
|
||||
autocompleteChannels: jest.fn(),
|
||||
searchAssociatedGroupsForReference: jest.fn(),
|
||||
fetchAgents: jest.fn(),
|
||||
},
|
||||
useChannelMentions: true,
|
||||
tabIndex: 0,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import type {Dispatch} from 'redux';
|
|||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {getAgents} from 'mattermost-redux/actions/agents';
|
||||
import Permissions from 'mattermost-redux/constants/permissions';
|
||||
import {getDefaultAgent} from 'mattermost-redux/selectors/entities/agents';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getAssociatedGroupsForReference} from 'mattermost-redux/selectors/entities/groups';
|
||||
import {makeGetProfilesForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
|
@ -47,6 +49,7 @@ const makeMapStateToProps = () => {
|
|||
autocompleteGroups,
|
||||
priorityProfiles: getProfilesForThread(state, ownProps.rootId ?? ''),
|
||||
delayChannelAutocomplete: getConfig(state).DelayChannelAutocomplete === 'true',
|
||||
defaultAgent: getDefaultAgent(state),
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -56,6 +59,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|||
autocompleteUsersInChannel,
|
||||
autocompleteChannels,
|
||||
searchAssociatedGroupsForReference,
|
||||
fetchAgents: getAgents,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import React from 'react';
|
|||
import type {ChangeEvent, ElementType, FocusEvent, KeyboardEvent, MouseEvent} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {Agent} from '@mattermost/types/agents';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {Group} from '@mattermost/types/groups';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
|
@ -64,11 +65,13 @@ export type Props = {
|
|||
autocompleteUsersInChannel: (prefix: string, channelId: string) => Promise<ActionResult>;
|
||||
autocompleteChannels: (term: string, success: (channels: Channel[]) => void, error: () => void) => Promise<ActionResult>;
|
||||
searchAssociatedGroupsForReference: (prefix: string, teamId: string, channelId: string | undefined) => Promise<{ data: any }>;
|
||||
fetchAgents: () => Promise<ActionResult>;
|
||||
};
|
||||
useChannelMentions: boolean;
|
||||
inputComponent?: ElementType;
|
||||
openWhenEmpty?: boolean;
|
||||
priorityProfiles?: UserProfile[];
|
||||
defaultAgent?: Agent;
|
||||
hasLabels?: boolean;
|
||||
hasError?: boolean;
|
||||
};
|
||||
|
|
@ -110,6 +113,7 @@ export default class Textbox extends React.PureComponent<Props> {
|
|||
autocompleteGroups: this.props.autocompleteGroups,
|
||||
searchAssociatedGroupsForReference: (prefix: string) => this.props.actions.searchAssociatedGroupsForReference(prefix, this.props.currentTeamId, this.props.channelId),
|
||||
priorityProfiles: this.props.priorityProfiles,
|
||||
defaultAgent: this.props.defaultAgent,
|
||||
}),
|
||||
new ChannelMentionProvider(props.actions.autocompleteChannels, props.delayChannelAutocomplete),
|
||||
new EmoticonProvider(),
|
||||
|
|
@ -129,6 +133,12 @@ export default class Textbox extends React.PureComponent<Props> {
|
|||
this.preview = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Fetch agents once when the component mounts to populate Redux store
|
||||
// This ensures defaultAgent is available for @ mention suggestions
|
||||
this.props.actions.fetchAgents();
|
||||
}
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onChange(e);
|
||||
};
|
||||
|
|
@ -139,7 +149,8 @@ export default class Textbox extends React.PureComponent<Props> {
|
|||
this.props.autocompleteGroups !== prevProps.autocompleteGroups ||
|
||||
this.props.useChannelMentions !== prevProps.useChannelMentions ||
|
||||
this.props.currentTeamId !== prevProps.currentTeamId ||
|
||||
this.props.priorityProfiles !== prevProps.priorityProfiles) {
|
||||
this.props.priorityProfiles !== prevProps.priorityProfiles ||
|
||||
this.props.defaultAgent !== prevProps.defaultAgent) {
|
||||
// Update channel id for AtMentionProvider.
|
||||
for (const provider of this.suggestionProviders) {
|
||||
if (provider instanceof AtMentionProvider) {
|
||||
|
|
@ -151,6 +162,7 @@ export default class Textbox extends React.PureComponent<Props> {
|
|||
autocompleteGroups: this.props.autocompleteGroups,
|
||||
searchAssociatedGroupsForReference: (prefix: string) => this.props.actions.searchAssociatedGroupsForReference(prefix, this.props.currentTeamId, this.props.channelId),
|
||||
priorityProfiles: this.props.priorityProfiles,
|
||||
defaultAgent: this.props.defaultAgent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
webapp/channels/src/components/widgets/tag/agent_tag.tsx
Normal file
31
webapp/channels/src/components/widgets/tag/agent_tag.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import Tag from './tag';
|
||||
import type {TagSize} from './tag';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
size?: TagSize;
|
||||
}
|
||||
|
||||
const AgentTag = ({className = '', size = 'xs'}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
return (
|
||||
<Tag
|
||||
uppercase={true}
|
||||
size={size}
|
||||
className={classNames('AgentTag', className)}
|
||||
text={formatMessage({
|
||||
id: 'tag.default.agent',
|
||||
defaultMessage: 'AGENT',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentTag;
|
||||
|
|
@ -6244,6 +6244,7 @@
|
|||
"system_policy_indicator.more_policies": "{count} more",
|
||||
"system_policy_indicator.multiple_policies_title": "Multiple system access policies applied to this {resourceType}",
|
||||
"system_policy_indicator.single_policy_title": "System access policy applied to this {resourceType}",
|
||||
"tag.default.agent": "AGENT",
|
||||
"tag.default.beta": "BETA",
|
||||
"tag.default.bot": "BOT",
|
||||
"tag.default.guest": "GUEST",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type {Agent, LLMService} from '@mattermost/types/agents';
|
|||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
export function getAgents(state: GlobalState): Agent[] {
|
||||
return state.entities.agents?.agents;
|
||||
return state.entities.agents?.agents || [];
|
||||
}
|
||||
|
||||
export function getAgentsStatus(state: GlobalState): {available: boolean; reason?: string} {
|
||||
|
|
@ -17,6 +17,11 @@ export function getAgent(state: GlobalState, agentId: string): Agent | undefined
|
|||
return agents.find((agent) => agent.id === agentId);
|
||||
}
|
||||
|
||||
export function getDefaultAgent(state: GlobalState): Agent | undefined {
|
||||
const agents = getAgents(state);
|
||||
return agents?.find((agent) => agent.is_default === true);
|
||||
}
|
||||
|
||||
export function getLLMServices(state: GlobalState): LLMService[] {
|
||||
return state.entities.agents?.llmServices || [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -384,3 +384,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.suggestion-list__item--default-agent {
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ export const TrialPeriodDays = {
|
|||
};
|
||||
|
||||
export const suitePluginIds = {
|
||||
agents: 'mattermost-ai',
|
||||
playbooks: 'playbooks',
|
||||
focalboard: 'focalboard',
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export type Agent = {
|
|||
username: string;
|
||||
service_id: string;
|
||||
service_type: string;
|
||||
is_default?: boolean;
|
||||
};
|
||||
|
||||
export type LLMService = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue