mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
MM-67953 Changed sorting of channels in some places to prioritize display name matches (#35679)
Some checks failed
API / build (push) Has been cancelled
Server CI / Compute Go Version (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Server CI / Check mocks (push) Has been cancelled
Server CI / Check go mod tidy (push) Has been cancelled
Server CI / check-style (push) Has been cancelled
Server CI / Check serialization methods for hot structs (push) Has been cancelled
Server CI / Vet API (push) Has been cancelled
Server CI / Check migration files (push) Has been cancelled
Server CI / Generate email templates (push) Has been cancelled
Server CI / Check store layers (push) Has been cancelled
Server CI / Check mmctl docs (push) Has been cancelled
Server CI / Postgres with binary parameters (push) Has been cancelled
Server CI / Postgres (push) Has been cancelled
Server CI / Postgres (FIPS) (push) Has been cancelled
Server CI / Generate Test Coverage (push) Has been cancelled
Server CI / Run mmctl tests (push) Has been cancelled
Server CI / Run mmctl tests (FIPS) (push) Has been cancelled
Server CI / Build mattermost server app (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
Some checks failed
API / build (push) Has been cancelled
Server CI / Compute Go Version (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Server CI / Check mocks (push) Has been cancelled
Server CI / Check go mod tidy (push) Has been cancelled
Server CI / check-style (push) Has been cancelled
Server CI / Check serialization methods for hot structs (push) Has been cancelled
Server CI / Vet API (push) Has been cancelled
Server CI / Check migration files (push) Has been cancelled
Server CI / Generate email templates (push) Has been cancelled
Server CI / Check store layers (push) Has been cancelled
Server CI / Check mmctl docs (push) Has been cancelled
Server CI / Postgres with binary parameters (push) Has been cancelled
Server CI / Postgres (push) Has been cancelled
Server CI / Postgres (FIPS) (push) Has been cancelled
Server CI / Generate Test Coverage (push) Has been cancelled
Server CI / Run mmctl tests (push) Has been cancelled
Server CI / Run mmctl tests (FIPS) (push) Has been cancelled
Server CI / Build mattermost server app (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
* MM-67953 Changed Browse Channels modal to prioritize channels with DisplayName matching search query * MM-67953 Changed channel shortlink autocomplete to prioritize non-member channels with DisplayName matching search query * Shared orderByDisplayNameMatch between both methods * Add E2E tests * Update existing E2E test to use new fixture * Run Prettier on E2E tests * And fix linting issue * And fix the fix... * Run server code linter
This commit is contained in:
parent
4c25d03f67
commit
5f8c77a3ef
8 changed files with 473 additions and 36 deletions
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class BrowseChannelsModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly createNewChannelButton: Locator;
|
||||
readonly hideJoinedCheckbox: Locator;
|
||||
readonly searchInput: Locator;
|
||||
|
||||
readonly results: Locator;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.createNewChannelButton = container.getByRole('button', {name: 'Create New Channel'});
|
||||
this.hideJoinedCheckbox = container.getByRole('checkbox', {name: 'Hide Joined'});
|
||||
this.searchInput = container.getByRole('textbox', {name: 'Search channels'});
|
||||
|
||||
// This role seems incorrect and will likely need to be changed later
|
||||
this.results = this.container.getByRole('search');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async toBeDoneLoading() {
|
||||
await expect(this.container.locator('.loading-screen')).toHaveCount(0);
|
||||
}
|
||||
|
||||
async toHaveNResults(count: number) {
|
||||
await expect(this.results.locator('.more-modal__row')).toHaveCount(count);
|
||||
}
|
||||
|
||||
async fillSearchInput(text: string) {
|
||||
await this.searchInput.fill(text);
|
||||
}
|
||||
|
||||
async toHaveChannelAsNthResult(channelName: string, index: number) {
|
||||
const row = this.results.locator('.more-modal__row').nth(index);
|
||||
|
||||
expect(await row.getAttribute('data-testid')).toEqual(`ChannelRow-${channelName}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import GlobalHeader from './global_header';
|
|||
import MainHeader from './main_header';
|
||||
import UserAccountMenu from './user_account_menu';
|
||||
// Channels Components
|
||||
import BrowseChannelsModal from './channels/browse_channels_modal';
|
||||
import ChannelsAppBar from './channels/app_bar';
|
||||
import ChannelsCenterView from './channels/center_view';
|
||||
import CreateTeamForm from './channels/create_team_form';
|
||||
|
|
@ -88,6 +89,7 @@ const components = {
|
|||
FindChannelsModal,
|
||||
FlagPostConfirmationDialog,
|
||||
NewChannelModal,
|
||||
BrowseChannelsModal,
|
||||
GenericConfirmModal,
|
||||
InvitePeopleModal,
|
||||
MembersInvitedModal,
|
||||
|
|
@ -158,6 +160,7 @@ export {
|
|||
FindChannelsModal,
|
||||
FlagPostConfirmationDialog,
|
||||
NewChannelModal,
|
||||
BrowseChannelsModal,
|
||||
GenericConfirmModal,
|
||||
InvitePeopleModal,
|
||||
MembersInvitedModal,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {expect, Page} from '@playwright/test';
|
|||
import {waitUntil} from 'async-wait-until';
|
||||
|
||||
import {
|
||||
BrowseChannelsModal,
|
||||
ChannelsPost,
|
||||
ChannelSettingsModal,
|
||||
CreateTeamForm,
|
||||
|
|
@ -36,6 +37,7 @@ export default class ChannelsPage {
|
|||
readonly deletePostModal;
|
||||
readonly findChannelsModal;
|
||||
readonly newChannelModal;
|
||||
readonly browseChannelsModal;
|
||||
public invitePeopleModal: InvitePeopleModal | undefined;
|
||||
public membersInvitedModal: MembersInvitedModal | undefined;
|
||||
readonly profileModal;
|
||||
|
|
@ -72,7 +74,8 @@ export default class ChannelsPage {
|
|||
this.createTeamForm = new CreateTeamForm(page.locator('.signup-team__container'));
|
||||
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
|
||||
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
|
||||
this.newChannelModal = new NewChannelModal(page.locator('#new-channel-modal'));
|
||||
this.newChannelModal = new NewChannelModal(page.getByRole('dialog', {name: 'Create a new channel'}));
|
||||
this.browseChannelsModal = new BrowseChannelsModal(page.getByRole('dialog', {name: 'Browse Channels'}));
|
||||
this.profileModal = new components.ProfileModal(page.getByRole('dialog', {name: 'Profile'}));
|
||||
this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'}));
|
||||
this.teamSettingsModal = new components.TeamSettingsModal(page.getByRole('dialog', {name: 'Team Settings'}));
|
||||
|
|
@ -208,12 +211,20 @@ export default class ChannelsPage {
|
|||
|
||||
async openNewChannelModal(): Promise<NewChannelModal> {
|
||||
await this.sidebarLeft.browseOrCreateChannelButton.click();
|
||||
await this.page.locator('#createNewChannelMenuItem').click();
|
||||
await this.page.getByText('Create new channel').click();
|
||||
await this.newChannelModal.toBeVisible();
|
||||
|
||||
return this.newChannelModal;
|
||||
}
|
||||
|
||||
async openBrowseChannelsModal(): Promise<BrowseChannelsModal> {
|
||||
await this.sidebarLeft.browseOrCreateChannelButton.click();
|
||||
await this.page.getByText('Browse channels').click();
|
||||
await this.browseChannelsModal.toBeVisible();
|
||||
|
||||
return this.browseChannelsModal;
|
||||
}
|
||||
|
||||
async openCreateTeamForm(): Promise<CreateTeamForm> {
|
||||
await this.sidebarLeft.teamMenuButton.click();
|
||||
await this.teamMenu.toBeVisible();
|
||||
|
|
|
|||
|
|
@ -40,32 +40,22 @@ test(
|
|||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Click on Browse or Create Channel button and then Browse Channels
|
||||
await channelsPage.sidebarLeft.browseOrCreateChannelButton.click();
|
||||
const browseChannelsMenuItem = page.locator('#browseChannelsMenuItem');
|
||||
await browseChannelsMenuItem.click();
|
||||
|
||||
// * Verify the Browse Channels dialog is visible
|
||||
const dialog = page.getByRole('dialog', {name: 'Browse Channels'});
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// * Verify the heading
|
||||
await expect(dialog.getByRole('heading', {name: 'Browse Channels'})).toBeVisible();
|
||||
// # Open the Browse Channels modal
|
||||
const dialog = await channelsPage.openBrowseChannelsModal();
|
||||
|
||||
// * Verify the search input exists
|
||||
const searchInput = dialog.getByPlaceholder('Search channels');
|
||||
const searchInput = dialog.searchInput;
|
||||
await expect(searchInput).toBeVisible();
|
||||
|
||||
// # Wait for channel list to load
|
||||
const channelList = dialog.locator('#moreChannelsList');
|
||||
await expect(channelList).toBeVisible();
|
||||
await dialog.toBeDoneLoading();
|
||||
|
||||
// # Hide already joined channels
|
||||
const hideJoinedCheckbox = dialog.getByText('Hide Joined');
|
||||
const hideJoinedCheckbox = dialog.hideJoinedCheckbox;
|
||||
await hideJoinedCheckbox.click();
|
||||
|
||||
// # Focus on Create Channel button and tab through elements
|
||||
const createChannelButton = dialog.locator('#createNewChannelButton');
|
||||
const createChannelButton = dialog.createNewChannelButton;
|
||||
await createChannelButton.focus();
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
|
|
@ -74,8 +64,9 @@ test(
|
|||
await page.keyboard.press('Tab');
|
||||
|
||||
// * Verify channel name is highlighted and has proper aria-label
|
||||
await dialog.toHaveChannelAsNthResult(channel1.name, 0);
|
||||
const channel1AriaLabel = `${channel1.display_name.toLowerCase()}, ${channel1.purpose.toLowerCase()}`;
|
||||
const channel1Item = dialog.getByLabel(channel1AriaLabel);
|
||||
const channel1Item = dialog.container.getByLabel(channel1AriaLabel);
|
||||
await expect(channel1Item).toBeVisible();
|
||||
await expect(channel1Item).toBeFocused();
|
||||
|
||||
|
|
@ -83,8 +74,9 @@ test(
|
|||
await page.keyboard.press('Tab');
|
||||
|
||||
// * Verify focus moved to next channel
|
||||
await dialog.toHaveChannelAsNthResult(channel2.name, 1);
|
||||
const channel2AriaLabel = `${channel2.display_name.toLowerCase()}, ${channel2.purpose.toLowerCase()}`;
|
||||
const channel2Item = dialog.getByLabel(channel2AriaLabel);
|
||||
const channel2Item = dialog.container.getByLabel(channel2AriaLabel);
|
||||
await expect(channel2Item).toBeFocused();
|
||||
},
|
||||
);
|
||||
|
|
@ -109,18 +101,11 @@ test(
|
|||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Click on Browse or Create Channel button and then Browse Channels
|
||||
await channelsPage.sidebarLeft.browseOrCreateChannelButton.click();
|
||||
const browseChannelsMenuItem = page.locator('#browseChannelsMenuItem');
|
||||
await browseChannelsMenuItem.click();
|
||||
|
||||
// * Verify the Browse Channels dialog is visible
|
||||
const dialog = page.getByRole('dialog', {name: 'Browse Channels'});
|
||||
await expect(dialog).toBeVisible();
|
||||
await pw.wait(pw.duration.one_sec);
|
||||
// # Open the Browse Channels modal
|
||||
const dialog = await channelsPage.openBrowseChannelsModal();
|
||||
|
||||
// * Verify aria snapshot of Browse Channels dialog
|
||||
await expect(dialog).toMatchAriaSnapshot(`
|
||||
await expect(dialog.container).toMatchAriaSnapshot(`
|
||||
- dialog "Browse Channels":
|
||||
- document:
|
||||
- heading "Browse Channels" [level=1]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that the "Other Channels" group in the ~channel autocomplete in the message input prioritizes
|
||||
* channels whose DisplayName matches the search term.
|
||||
*/
|
||||
test(
|
||||
'MM-67953 channel mention autocomplete prioritizes DisplayName matches in Other Channels',
|
||||
{tag: ['@mentions']},
|
||||
async ({pw}) => {
|
||||
// # Initialize setup
|
||||
const {team, user, adminClient} = await pw.initSetup();
|
||||
|
||||
// # Create channels whose DisplayName matches the search term (user is NOT a member)
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-gamma-conversation-' + Date.now(),
|
||||
display_name: 'Gamma: Conversation',
|
||||
type: 'O',
|
||||
});
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-gamma-logs-' + Date.now(),
|
||||
display_name: 'Gamma: Logs',
|
||||
type: 'O',
|
||||
});
|
||||
|
||||
// # Create channels whose Purpose matches but DisplayName does NOT (user is NOT a member)
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-alpha-channel-' + Date.now(),
|
||||
display_name: 'Alpha Channel',
|
||||
type: 'O',
|
||||
purpose: 'alpha release of gamma',
|
||||
});
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-beta-channel-' + Date.now(),
|
||||
display_name: 'Beta Channel',
|
||||
type: 'O',
|
||||
purpose: 'beta release of gamma',
|
||||
});
|
||||
|
||||
// # Log in as regular user
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Visit town-square channel
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Type a channel mention in the message input to trigger autocomplete
|
||||
await channelsPage.centerView.postCreate.writeMessage('~gamma');
|
||||
|
||||
// # Wait for the suggestion list to appear
|
||||
const suggestionList = channelsPage.centerView.postCreate.suggestionList;
|
||||
await expect(suggestionList).toBeVisible();
|
||||
|
||||
// # Get all suggestion items within the "Other Channels" group
|
||||
const otherChannelsGroup = suggestionList.getByRole('group', {name: 'Other Channels'});
|
||||
await expect(otherChannelsGroup).toBeVisible();
|
||||
|
||||
const suggestions = otherChannelsGroup.getByRole('option');
|
||||
|
||||
// * Verify at least 4 suggestions are shown in "Other Channels"
|
||||
await expect(suggestions).toHaveCount(4);
|
||||
|
||||
// * Verify DisplayName-matching channels appear first, sorted alphabetically
|
||||
await expect(suggestions.nth(0)).toContainText('Gamma: Conversation');
|
||||
await expect(suggestions.nth(1)).toContainText('Gamma: Logs');
|
||||
|
||||
// * Verify Purpose-only-matching channels appear after, sorted alphabetically by DisplayName
|
||||
await expect(suggestions.nth(2)).toContainText('Alpha Channel');
|
||||
await expect(suggestions.nth(3)).toContainText('Beta Channel');
|
||||
},
|
||||
);
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that Browse Channels modal search results prioritize channels whose DisplayName matches the search
|
||||
* term over channels that only match on other fields.
|
||||
*/
|
||||
test(
|
||||
'MM-67953 Browse Channels modal prioritizes DisplayName matches in search results',
|
||||
{tag: ['@browse_channels']},
|
||||
async ({pw}) => {
|
||||
// # Initialize setup
|
||||
const {team, user, adminClient} = await pw.initSetup();
|
||||
|
||||
// # Create channels whose DisplayName matches the search term
|
||||
const displayMatchA = await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-gamma-conversation-' + Date.now(),
|
||||
display_name: 'Gamma: Conversation',
|
||||
type: 'O',
|
||||
});
|
||||
const displayMatchB = await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-gamma-logs-' + Date.now(),
|
||||
display_name: 'Gamma: Logs',
|
||||
type: 'O',
|
||||
});
|
||||
|
||||
// # Create channels whose Purpose matches but DisplayName does NOT
|
||||
const purposeMatchA = await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-alpha-channel-' + Date.now(),
|
||||
display_name: 'Alpha Channel',
|
||||
type: 'O',
|
||||
purpose: 'alpha release of gamma',
|
||||
});
|
||||
const purposeMatchB = await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-beta-channel-' + Date.now(),
|
||||
display_name: 'Beta Channel',
|
||||
type: 'O',
|
||||
purpose: 'beta release of gamma',
|
||||
});
|
||||
|
||||
// # Log in as regular user
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Visit town-square channel
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Browse Channels modal
|
||||
const dialog = await channelsPage.openBrowseChannelsModal();
|
||||
await dialog.toBeVisible();
|
||||
|
||||
// # Search for the term that matches DisplayName on some channels and Purpose on others
|
||||
await dialog.fillSearchInput('gamma');
|
||||
await dialog.toBeDoneLoading();
|
||||
await dialog.toHaveNResults(4);
|
||||
|
||||
// DisplayName matches come first, sorted alphabetically
|
||||
await dialog.toHaveChannelAsNthResult(displayMatchA.name, 0);
|
||||
await dialog.toHaveChannelAsNthResult(displayMatchB.name, 1);
|
||||
|
||||
// Purpose-only matches come after, sorted alphabetically by DisplayName
|
||||
await dialog.toHaveChannelAsNthResult(purposeMatchA.name, 2);
|
||||
await dialog.toHaveChannelAsNthResult(purposeMatchB.name, 3);
|
||||
},
|
||||
);
|
||||
|
|
@ -3036,9 +3036,7 @@ func (s SqlChannelStore) Autocomplete(rctx request.CTX, userID, term string, inc
|
|||
sq.Expr("c.TeamId = t.id"),
|
||||
sq.Expr("t.id = tm.TeamId"),
|
||||
sq.Eq{"tm.UserId": userID},
|
||||
}).
|
||||
OrderBy("c.DisplayName").
|
||||
Limit(model.ChannelSearchDefaultLimit)
|
||||
})
|
||||
|
||||
// Always filter out soft-deleted team memberships - users removed from
|
||||
// a team should not see channels from that team regardless of includeDeleted
|
||||
|
|
@ -3069,6 +3067,10 @@ func (s SqlChannelStore) Autocomplete(rctx request.CTX, userID, term string, inc
|
|||
query = query.Where(searchClause)
|
||||
}
|
||||
|
||||
query = orderByDisplayNameMatch(query, term)
|
||||
|
||||
query = query.Limit(model.ChannelSearchDefaultLimit)
|
||||
|
||||
sql, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Autocomplete_Tosql")
|
||||
|
|
@ -3086,7 +3088,6 @@ func (s SqlChannelStore) AutocompleteInTeam(rctx request.CTX, teamID, userID, te
|
|||
query := s.getQueryBuilder().Select(channelSliceColumns(true, "c")...).
|
||||
From("Channels c").
|
||||
Where(sq.Eq{"c.TeamId": teamID}).
|
||||
OrderBy("c.DisplayName").
|
||||
Limit(model.ChannelSearchDefaultLimit)
|
||||
|
||||
if !includeDeleted {
|
||||
|
|
@ -3114,6 +3115,8 @@ func (s SqlChannelStore) AutocompleteInTeam(rctx request.CTX, teamID, userID, te
|
|||
query = query.Where(searchClause)
|
||||
}
|
||||
|
||||
query = orderByDisplayNameMatch(query, term)
|
||||
|
||||
return s.performSearch(query, term)
|
||||
}
|
||||
|
||||
|
|
@ -3583,6 +3586,18 @@ func (s SqlChannelStore) searchClause(term string) sq.Sqlizer {
|
|||
}
|
||||
}
|
||||
|
||||
// orderByDisplayNameMatch adds an ORDER BY clause that prioritizes channels whose DisplayName matches the search term,
|
||||
// then sorts alphabetically by DisplayName.
|
||||
func orderByDisplayNameMatch(query sq.SelectBuilder, term string) sq.SelectBuilder {
|
||||
sanitizedTerm := sanitizeSearchTerm(term, "*")
|
||||
if sanitizedTerm == "" {
|
||||
return query.OrderBy("c.DisplayName")
|
||||
}
|
||||
|
||||
likeTerm := wildcardSearchTerm(sanitizedTerm)
|
||||
return query.OrderByClause("CASE WHEN LOWER(c.DisplayName) LIKE LOWER(?) ESCAPE '*' THEN 0 ELSE 1 END, c.DisplayName", likeTerm)
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) searchGroupChannelsQuery(userId, term string) sq.SelectBuilder {
|
||||
baseLikeTerm := "ARRAY_TO_STRING(ARRAY_AGG(u.Username), ', ') LIKE ?"
|
||||
terms := strings.Fields((strings.ToLower(term)))
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ func TestChannelStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore
|
|||
t.Run("GetMemberCountsByGroup", func(t *testing.T) { testGetMemberCountsByGroup(t, rctx, ss) })
|
||||
t.Run("GetGuestCount", func(t *testing.T) { testGetGuestCount(t, rctx, ss) })
|
||||
t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, rctx, ss) })
|
||||
t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, rctx, ss) })
|
||||
t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, rctx, ss, s) })
|
||||
t.Run("Autocomplete", func(t *testing.T) { testAutocomplete(t, rctx, ss, s) })
|
||||
t.Run("SearchForUserInTeam", func(t *testing.T) { testChannelStoreSearchForUserInTeam(t, rctx, ss) })
|
||||
t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, rctx, ss) })
|
||||
|
|
@ -5990,7 +5990,7 @@ func testChannelStoreSearchMore(t *testing.T, rctx request.CTX, ss store.Store)
|
|||
})
|
||||
}
|
||||
|
||||
func testChannelStoreSearchInTeam(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
func testChannelStoreSearchInTeam(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
||||
teamID := model.NewId()
|
||||
otherTeamID := model.NewId()
|
||||
|
||||
|
|
@ -6206,6 +6206,138 @@ func testChannelStoreSearchInTeam(t *testing.T, rctx request.CTX, ss store.Store
|
|||
require.ElementsMatch(t, testCase.ExpectedResults, channels)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("AutoCompleteInTeam/DisplayName matches are prioritized in ordering", func(t *testing.T) {
|
||||
s.GetMaster().Exec("TRUNCATE Channels")
|
||||
|
||||
sortTeam := &model.Team{
|
||||
DisplayName: "sort-team",
|
||||
Name: NewTestID(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
sortTeam, err := ss.Team().Save(sortTeam)
|
||||
require.NoError(t, err)
|
||||
|
||||
sortUser := model.NewId()
|
||||
_, err = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: sortTeam.Id, UserId: sortUser}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Channels where DisplayName matches "alpha":
|
||||
chDisplayAlpha2 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Alpha Two", Name: NewTestID(), Type: model.ChannelTypeOpen}
|
||||
_, err = ss.Channel().Save(rctx, &chDisplayAlpha2, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
chDisplayAlpha1 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Alpha One", Name: NewTestID(), Type: model.ChannelTypeOpen}
|
||||
_, err = ss.Channel().Save(rctx, &chDisplayAlpha1, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Channels where only Purpose matches "alpha" (DisplayName does NOT contain "alpha"):
|
||||
chPurposeOnly2 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Zulu Channel", Name: NewTestID(), Purpose: "alpha related work", Type: model.ChannelTypeOpen}
|
||||
_, err = ss.Channel().Save(rctx, &chPurposeOnly2, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
chPurposeOnly1 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Bravo Channel", Name: NewTestID(), Purpose: "alpha discussions", Type: model.ChannelTypeOpen}
|
||||
_, err = ss.Channel().Save(rctx, &chPurposeOnly1, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
channels, err := ss.Channel().AutocompleteInTeam(rctx, sortTeam.Id, sortUser, "alpha", false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, channels, 4)
|
||||
|
||||
// DisplayName matches come first, sorted alphabetically by DisplayName
|
||||
assert.Equal(t, chDisplayAlpha1.Id, channels[0].Id, "first should be Alpha One (DisplayName match, alphabetically first)")
|
||||
assert.Equal(t, chDisplayAlpha2.Id, channels[1].Id, "second should be Alpha Two (DisplayName match, alphabetically second)")
|
||||
|
||||
// Non-DisplayName matches come second, also sorted alphabetically by DisplayName
|
||||
assert.Equal(t, chPurposeOnly1.Id, channels[2].Id, "third should be Bravo Channel (no DisplayName match, alphabetically first)")
|
||||
assert.Equal(t, chPurposeOnly2.Id, channels[3].Id, "fourth should be Zulu Channel (no DisplayName match, alphabetically second)")
|
||||
})
|
||||
|
||||
t.Run("AutoCompleteInTeam/no DisplayName matches still returns results sorted by DisplayName", func(t *testing.T) {
|
||||
s.GetMaster().Exec("TRUNCATE Channels")
|
||||
|
||||
sortTeam := &model.Team{
|
||||
DisplayName: "sort-team",
|
||||
Name: NewTestID(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
sortTeam, err := ss.Team().Save(sortTeam)
|
||||
require.NoError(t, err)
|
||||
|
||||
sortUser := model.NewId()
|
||||
_, err = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: sortTeam.Id, UserId: sortUser}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// All channels match on Purpose only, not DisplayName
|
||||
ch1 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Zulu Display", Name: NewTestID(), Purpose: "searchterm stuff", Type: model.ChannelTypeOpen}
|
||||
_, err = ss.Channel().Save(rctx, &ch1, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
ch2 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Alpha Display", Name: NewTestID(), Purpose: "searchterm things", Type: model.ChannelTypeOpen}
|
||||
_, err = ss.Channel().Save(rctx, &ch2, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
ch3 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Mike Display", Name: NewTestID(), Purpose: "searchterm items", Type: model.ChannelTypeOpen}
|
||||
_, err = ss.Channel().Save(rctx, &ch3, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
channels, err := ss.Channel().AutocompleteInTeam(rctx, sortTeam.Id, sortUser, "searchterm", false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, channels, 3)
|
||||
|
||||
// All in the same priority bucket (no DisplayName match), sorted by DisplayName
|
||||
assert.Equal(t, ch2.Id, channels[0].Id, "Alpha Display should be first")
|
||||
assert.Equal(t, ch3.Id, channels[1].Id, "Mike Display should be second")
|
||||
assert.Equal(t, ch1.Id, channels[2].Id, "Zulu Display should be third")
|
||||
})
|
||||
|
||||
t.Run("AutoCompleteInTeam/DisplayName match is not cut off by limit", func(t *testing.T) {
|
||||
s.GetMaster().Exec("TRUNCATE Channels")
|
||||
|
||||
sortTeam := &model.Team{
|
||||
DisplayName: "sort-team",
|
||||
Name: NewTestID(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
sortTeam, err := ss.Team().Save(sortTeam)
|
||||
require.NoError(t, err)
|
||||
|
||||
sortUser := model.NewId()
|
||||
_, err = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: sortTeam.Id, UserId: sortUser}, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create 50 channels that match only on Purpose (not DisplayName).
|
||||
for i := range model.ChannelSearchDefaultLimit {
|
||||
_, err = ss.Channel().Save(rctx, &model.Channel{
|
||||
TeamId: sortTeam.Id,
|
||||
DisplayName: fmt.Sprintf("AAA Channel %03d", i),
|
||||
Name: NewTestID(),
|
||||
Purpose: "findme in purpose",
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Create 1 channel whose DisplayName matches the term but sorts last alphabetically.
|
||||
chDisplayMatch := model.Channel{
|
||||
TeamId: sortTeam.Id,
|
||||
DisplayName: "ZZZ findme channel",
|
||||
Name: NewTestID(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}
|
||||
_, err = ss.Channel().Save(rctx, &chDisplayMatch, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
channels, err := ss.Channel().AutocompleteInTeam(rctx, sortTeam.Id, sortUser, "findme", false, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, channels, model.ChannelSearchDefaultLimit)
|
||||
|
||||
// The DisplayName-matching channel must be first despite sorting last alphabetically.
|
||||
assert.Equal(t, chDisplayMatch.Id, channels[0].Id, "DisplayName match should be returned first, not cut off by limit")
|
||||
})
|
||||
}
|
||||
|
||||
func testAutocomplete(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
||||
|
|
@ -6419,6 +6551,101 @@ func testAutocomplete(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("channels with a matching DisplayName are sorted before those matching other fields", func(t *testing.T) {
|
||||
// Clean slate for ordering tests
|
||||
s.GetMaster().Exec("TRUNCATE Channels")
|
||||
|
||||
sortTeam := &model.Team{
|
||||
DisplayName: "sort-team",
|
||||
Name: NewTestID(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
sortTeam, err2 := ss.Team().Save(sortTeam)
|
||||
require.NoError(t, err2)
|
||||
|
||||
sortUser := model.NewId()
|
||||
_, err2 = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: sortTeam.Id, UserId: sortUser}, -1)
|
||||
require.NoError(t, err2)
|
||||
|
||||
// Channels where DisplayName matches "alpha":
|
||||
chDisplayAlpha2 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Alpha Two", Name: NewTestID(), Type: model.ChannelTypeOpen}
|
||||
_, err2 = ss.Channel().Save(rctx, &chDisplayAlpha2, -1)
|
||||
require.NoError(t, err2)
|
||||
|
||||
chDisplayAlpha1 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Alpha One", Name: NewTestID(), Type: model.ChannelTypeOpen}
|
||||
_, err2 = ss.Channel().Save(rctx, &chDisplayAlpha1, -1)
|
||||
require.NoError(t, err2)
|
||||
|
||||
// Channels where only Purpose matches "alpha" (DisplayName does NOT contain "alpha"):
|
||||
chPurposeOnly2 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Zulu Channel", Name: NewTestID(), Purpose: "alpha related work", Type: model.ChannelTypeOpen}
|
||||
_, err2 = ss.Channel().Save(rctx, &chPurposeOnly2, -1)
|
||||
require.NoError(t, err2)
|
||||
|
||||
chPurposeOnly1 := model.Channel{TeamId: sortTeam.Id, DisplayName: "Bravo Channel", Name: NewTestID(), Purpose: "alpha discussions", Type: model.ChannelTypeOpen}
|
||||
_, err2 = ss.Channel().Save(rctx, &chPurposeOnly1, -1)
|
||||
require.NoError(t, err2)
|
||||
|
||||
channels, err2 := ss.Channel().Autocomplete(rctx, sortUser, "alpha", false, false)
|
||||
require.NoError(t, err2)
|
||||
require.Len(t, channels, 4)
|
||||
|
||||
// DisplayName matches come first, sorted alphabetically by DisplayName
|
||||
assert.Equal(t, chDisplayAlpha1.Id, channels[0].Id, "first should be Alpha One (DisplayName match, alphabetically first)")
|
||||
assert.Equal(t, chDisplayAlpha2.Id, channels[1].Id, "second should be Alpha Two (DisplayName match, alphabetically second)")
|
||||
|
||||
// Non-DisplayName matches come second, also sorted alphabetically by DisplayName
|
||||
assert.Equal(t, chPurposeOnly1.Id, channels[2].Id, "third should be Bravo Channel (no DisplayName match, alphabetically first)")
|
||||
assert.Equal(t, chPurposeOnly2.Id, channels[3].Id, "fourth should be Zulu Channel (no DisplayName match, alphabetically second)")
|
||||
})
|
||||
|
||||
t.Run("channels with a matching DisplayName are sorted before those matching other fields, even when the results are cut off", func(t *testing.T) {
|
||||
s.GetMaster().Exec("TRUNCATE Channels")
|
||||
|
||||
sortTeam := &model.Team{
|
||||
DisplayName: "sort-team",
|
||||
Name: NewTestID(),
|
||||
Email: MakeEmail(),
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
sortTeam, err2 := ss.Team().Save(sortTeam)
|
||||
require.NoError(t, err2)
|
||||
|
||||
sortUser := model.NewId()
|
||||
_, err2 = ss.Team().SaveMember(rctx, &model.TeamMember{TeamId: sortTeam.Id, UserId: sortUser}, -1)
|
||||
require.NoError(t, err2)
|
||||
|
||||
// Create 50 channels that match only on Purpose (not DisplayName).
|
||||
// Give them DisplayNames that sort before the DisplayName-matching channel.
|
||||
for i := range model.ChannelSearchDefaultLimit {
|
||||
_, err2 = ss.Channel().Save(rctx, &model.Channel{
|
||||
TeamId: sortTeam.Id,
|
||||
DisplayName: fmt.Sprintf("AAA Channel %03d", i),
|
||||
Name: NewTestID(),
|
||||
Purpose: "findme in purpose",
|
||||
Type: model.ChannelTypeOpen,
|
||||
}, -1)
|
||||
require.NoError(t, err2)
|
||||
}
|
||||
|
||||
// Create 1 channel whose DisplayName matches the term but sorts last alphabetically.
|
||||
chDisplayMatch := model.Channel{
|
||||
TeamId: sortTeam.Id,
|
||||
DisplayName: "ZZZ findme channel",
|
||||
Name: NewTestID(),
|
||||
Type: model.ChannelTypeOpen,
|
||||
}
|
||||
_, err2 = ss.Channel().Save(rctx, &chDisplayMatch, -1)
|
||||
require.NoError(t, err2)
|
||||
|
||||
channels, err2 := ss.Channel().Autocomplete(rctx, sortUser, "findme", false, false)
|
||||
require.NoError(t, err2)
|
||||
require.Len(t, channels, model.ChannelSearchDefaultLimit)
|
||||
|
||||
// The DisplayName-matching channel must be first despite sorting last alphabetically.
|
||||
assert.Equal(t, chDisplayMatch.Id, channels[0].Id, "DisplayName match should be returned first, not cut off by limit")
|
||||
})
|
||||
|
||||
t.Run("Limit", func(t *testing.T) {
|
||||
for i := range model.ChannelSearchDefaultLimit + 10 {
|
||||
_, err = ss.Channel().Save(rctx, &model.Channel{
|
||||
|
|
|
|||
Loading…
Reference in a new issue