mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-47181] Migrate "components/suggestion/at_mention_provider" and associated tests to Typescript (#25342)
This commit is contained in:
parent
8f0abc1316
commit
53298a2d60
@ -8,7 +8,7 @@ exports[`components/TextBox should match snapshot with additional, optional prop
|
||||
className="form-control custom-textarea textbox-preview-area"
|
||||
onBlur={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyPress={[MockFunction]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Connect(PostMarkdown)
|
||||
@ -52,10 +52,10 @@ exports[`components/TextBox should match snapshot with additional, optional prop
|
||||
listPosition="top"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onComposition={[Function]}
|
||||
onHeightChange={[Function]}
|
||||
onComposition={[MockFunction]}
|
||||
onHeightChange={[MockFunction]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyPress={[MockFunction]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
openWhenEmpty={true}
|
||||
@ -66,10 +66,34 @@ exports[`components/TextBox should match snapshot with additional, optional prop
|
||||
"addLastViewAtToProfiles": [Function],
|
||||
"autocompleteGroups": Array [
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "gid1",
|
||||
"member_count": 0,
|
||||
"name": "group_name",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
},
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "gid2",
|
||||
"member_count": 0,
|
||||
"name": "group_name",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
},
|
||||
],
|
||||
"autocompleteUsersInChannel": [Function],
|
||||
@ -136,7 +160,7 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
|
||||
className="form-control custom-textarea textbox-preview-area"
|
||||
onBlur={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyPress={[MockFunction]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Connect(PostMarkdown)
|
||||
@ -179,7 +203,7 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyPress={[MockFunction]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
placeholder="placeholder text"
|
||||
@ -189,10 +213,34 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
|
||||
"addLastViewAtToProfiles": [Function],
|
||||
"autocompleteGroups": Array [
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "gid1",
|
||||
"member_count": 0,
|
||||
"name": "group_name",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
},
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "gid2",
|
||||
"member_count": 0,
|
||||
"name": "group_name",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
},
|
||||
],
|
||||
"autocompleteUsersInChannel": [Function],
|
||||
@ -259,7 +307,7 @@ exports[`components/TextBox should throw error when new property is too long 1`]
|
||||
className="form-control custom-textarea textbox-preview-area"
|
||||
onBlur={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyPress={[MockFunction]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Connect(PostMarkdown)
|
||||
@ -302,7 +350,7 @@ exports[`components/TextBox should throw error when new property is too long 1`]
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyPress={[MockFunction]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
placeholder="placeholder text"
|
||||
@ -312,10 +360,34 @@ exports[`components/TextBox should throw error when new property is too long 1`]
|
||||
"addLastViewAtToProfiles": [Function],
|
||||
"autocompleteGroups": Array [
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "gid1",
|
||||
"member_count": 0,
|
||||
"name": "group_name",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
},
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "gid2",
|
||||
"member_count": 0,
|
||||
"name": "group_name",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
},
|
||||
],
|
||||
"autocompleteUsersInChannel": [Function],
|
||||
@ -382,7 +454,7 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
|
||||
className="form-control custom-textarea textbox-preview-area"
|
||||
onBlur={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyPress={[MockFunction]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Connect(PostMarkdown)
|
||||
@ -425,7 +497,7 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyPress={[MockFunction]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
placeholder="placeholder text"
|
||||
@ -435,10 +507,34 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
|
||||
"addLastViewAtToProfiles": [Function],
|
||||
"autocompleteGroups": Array [
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "gid1",
|
||||
"member_count": 0,
|
||||
"name": "group_name",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
},
|
||||
Object {
|
||||
"allow_reference": true,
|
||||
"create_at": 1,
|
||||
"delete_at": 0,
|
||||
"description": "",
|
||||
"display_name": "group_display_name",
|
||||
"has_syncables": false,
|
||||
"id": "gid2",
|
||||
"member_count": 0,
|
||||
"name": "group_name",
|
||||
"remote_id": "",
|
||||
"scheme_admin": false,
|
||||
"source": "",
|
||||
"update_at": 1,
|
||||
},
|
||||
],
|
||||
"autocompleteUsersInChannel": [Function],
|
||||
|
@ -1,35 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AtMentionProvider from 'components/suggestion/at_mention_provider/at_mention_provider.jsx';
|
||||
import AtMentionProvider, {type Props} from 'components/suggestion/at_mention_provider/at_mention_provider';
|
||||
import AtMentionSuggestion from 'components/suggestion/at_mention_provider/at_mention_suggestion';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
|
||||
const userid10 = {id: 'userid10', username: 'nicknamer', first_name: '', last_name: '', nickname: 'Z'};
|
||||
const userid3 = {id: 'userid3', username: 'other', first_name: 'X', last_name: 'Y', nickname: 'Z'};
|
||||
const userid1 = {id: 'userid1', username: 'user', first_name: 'a', last_name: 'b', nickname: 'c', isCurrentUser: true};
|
||||
const userid2 = {id: 'userid2', username: 'user2', first_name: 'd', last_name: 'e', nickname: 'f'};
|
||||
const userid4 = {id: 'userid4', username: 'user4', first_name: 'X', last_name: 'Y', nickname: 'Z'};
|
||||
const userid5 = {id: 'userid5', username: 'user5', first_name: 'out', last_name: 'out', nickname: 'out'};
|
||||
const userid6 = {id: 'userid6', username: 'user6.six-split', first_name: 'out Junior', last_name: 'out', nickname: 'out'};
|
||||
const userid7 = {id: 'userid7', username: 'xuser7', first_name: '', last_name: '', nickname: 'x'};
|
||||
const userid8 = {id: 'userid8', username: 'xuser8', first_name: 'Robert', last_name: 'Ward', nickname: 'nickname'};
|
||||
const userid10 = TestHelper.getUserMock({id: 'userid10', username: 'nicknamer', first_name: '', last_name: '', nickname: 'Z'});
|
||||
const userid3 = TestHelper.getUserMock({id: 'userid3', username: 'other', first_name: 'X', last_name: 'Y', nickname: 'Z'});
|
||||
const userid1 = {...TestHelper.getUserMock({id: 'userid1', username: 'user', first_name: 'a', last_name: 'b', nickname: 'c'}), isCurrentUser: true};
|
||||
const userid2 = TestHelper.getUserMock({id: 'userid2', username: 'user2', first_name: 'd', last_name: 'e', nickname: 'f'});
|
||||
const userid4 = TestHelper.getUserMock({id: 'userid4', username: 'user4', first_name: 'X', last_name: 'Y', nickname: 'Z'});
|
||||
const userid5 = TestHelper.getUserMock({id: 'userid5', username: 'user5', first_name: 'out', last_name: 'out', nickname: 'out'});
|
||||
const userid6 = TestHelper.getUserMock({id: 'userid6', username: 'user6.six-split', first_name: 'out Junior', last_name: 'out', nickname: 'out'});
|
||||
const userid7 = TestHelper.getUserMock({id: 'userid7', username: 'xuser7', first_name: '', last_name: '', nickname: 'x'});
|
||||
const userid8 = TestHelper.getUserMock({id: 'userid8', username: 'xuser8', first_name: 'Robert', last_name: 'Ward', nickname: 'nickname'});
|
||||
|
||||
const groupid1 = {id: 'groupid1', name: 'board', display_name: 'board'};
|
||||
const groupid2 = {id: 'groupid2', name: 'developers', display_name: 'developers'};
|
||||
const groupid3 = {id: 'groupid3', name: 'software-engineers', display_name: 'software engineers'};
|
||||
const groupid1 = TestHelper.getGroupMock({id: 'groupid1', name: 'board', display_name: 'board'});
|
||||
const groupid2 = TestHelper.getGroupMock({id: 'groupid2', name: 'developers', display_name: 'developers'});
|
||||
const groupid3 = TestHelper.getGroupMock({id: 'groupid3', name: 'software-engineers', display_name: 'software engineers'});
|
||||
|
||||
const baseParams = {
|
||||
const baseParams: Props = {
|
||||
currentUserId: 'userid1',
|
||||
channelId: 'channelid1',
|
||||
autocompleteUsersInChannel: jest.fn().mockResolvedValue(false),
|
||||
autocompleteGroups: [groupid1, groupid2, groupid3],
|
||||
useChannelMentions: true,
|
||||
searchAssociatedGroupsForReference: jest.fn().mockResolvedValue(false),
|
||||
priorityProfiles: [],
|
||||
};
|
||||
|
||||
it('should ignore pretexts that are not at-mentions', () => {
|
||||
@ -169,8 +171,8 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
|
||||
});
|
||||
|
||||
it('should have priorityProfiles at the top', async () => {
|
||||
const userid11 = {id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'};
|
||||
const userid12 = {id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'};
|
||||
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'});
|
||||
|
||||
const pretext = '@';
|
||||
const matchedPretext = '@';
|
||||
@ -315,8 +317,8 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
|
||||
});
|
||||
|
||||
it('should remove duplicates from results', async () => {
|
||||
const userid11 = {id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'};
|
||||
const userid12 = {id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'};
|
||||
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'});
|
||||
|
||||
const pretext = '@';
|
||||
const matchedPretext = '@';
|
||||
@ -461,8 +463,8 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
|
||||
});
|
||||
|
||||
it('should sort results based on last_viewed_at', async () => {
|
||||
const userid11 = {id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'};
|
||||
const userid12 = {id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'};
|
||||
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'});
|
||||
|
||||
const pretext = '@';
|
||||
const matchedPretext = '@';
|
@ -1,14 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Group} from '@mattermost/types/groups';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {Filters} from 'mattermost-redux/selectors/entities/users';
|
||||
import {makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
|
||||
import {makeAddLastViewAtToProfiles} from 'mattermost-redux/selectors/entities/utils';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {getSuggestionsSplitBy, getSuggestionsSplitByMultiple} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import store from 'stores/redux_store';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import AtMentionSuggestion from './at_mention_suggestion';
|
||||
|
||||
import Provider from '../provider';
|
||||
@ -16,14 +23,65 @@ import Provider from '../provider';
|
||||
const profilesInChannelOptions = {active: true};
|
||||
const regexForAtMention = /(?:^|\W)@([\p{L}\d\-_. ]*)$/iu;
|
||||
|
||||
type UserProfileWithLastViewAt = UserProfile & {last_viewed_at?: number};
|
||||
|
||||
type CreatedProfile = UserProfile & {
|
||||
type: string;
|
||||
isCurrentUser?: boolean;
|
||||
last_viewed_at?: number;
|
||||
};
|
||||
|
||||
type CreatedGroup = Group & {type: string};
|
||||
|
||||
type Results = {
|
||||
matchedPretext: string;
|
||||
terms: string[];
|
||||
items: any;
|
||||
component: React.ElementType;
|
||||
};
|
||||
|
||||
type ResultsCallback = (results: Results) => void;
|
||||
|
||||
export type Props = {
|
||||
currentUserId: string;
|
||||
channelId: string;
|
||||
autocompleteUsersInChannel: (prefix: string) => Promise<ActionResult>;
|
||||
useChannelMentions: boolean;
|
||||
autocompleteGroups: Group[] | null;
|
||||
searchAssociatedGroupsForReference: (prefix: string) => Promise<{data: any}>;
|
||||
priorityProfiles: UserProfile[] | undefined;
|
||||
}
|
||||
|
||||
// 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.
|
||||
export default class AtMentionProvider extends Provider {
|
||||
constructor(props) {
|
||||
public currentUserId: string;
|
||||
public channelId: string;
|
||||
public autocompleteUsersInChannel: (prefix: string) => Promise<ActionResult>;
|
||||
public useChannelMentions: boolean;
|
||||
public autocompleteGroups: Group[] | null;
|
||||
public searchAssociatedGroupsForReference: (prefix: string) => Promise<{data: any}>;
|
||||
public priorityProfiles: UserProfile[] | undefined;
|
||||
|
||||
public data: any;
|
||||
public lastCompletedWord: string;
|
||||
public lastPrefixWithNoResults: string;
|
||||
public getProfilesInChannel: (state: GlobalState, channelId: string, filters?: Filters | undefined) => UserProfile[];
|
||||
public addLastViewAtToProfiles: (state: GlobalState, profiles: UserProfile[]) => UserProfileWithLastViewAt[];
|
||||
|
||||
constructor(props: Props) {
|
||||
super();
|
||||
|
||||
this.setProps(props);
|
||||
const {currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles} = props;
|
||||
|
||||
this.currentUserId = currentUserId;
|
||||
this.channelId = channelId;
|
||||
this.autocompleteUsersInChannel = autocompleteUsersInChannel;
|
||||
this.useChannelMentions = useChannelMentions;
|
||||
this.autocompleteGroups = autocompleteGroups;
|
||||
this.searchAssociatedGroupsForReference = searchAssociatedGroupsForReference;
|
||||
this.priorityProfiles = priorityProfiles;
|
||||
|
||||
this.data = null;
|
||||
this.lastCompletedWord = '';
|
||||
@ -33,9 +91,7 @@ export default class AtMentionProvider extends Provider {
|
||||
this.addLastViewAtToProfiles = makeAddLastViewAtToProfiles();
|
||||
}
|
||||
|
||||
// setProps gives the provider additional context for matching pretexts. Ideally this would
|
||||
// just be something akin to a connected component with access to the store itself.
|
||||
setProps({currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles}) {
|
||||
setProps({currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles}: Props) {
|
||||
this.currentUserId = currentUserId;
|
||||
this.channelId = channelId;
|
||||
this.autocompleteUsersInChannel = autocompleteUsersInChannel;
|
||||
@ -61,8 +117,8 @@ export default class AtMentionProvider extends Provider {
|
||||
|
||||
// retrieves the parts of the profile that should be checked
|
||||
// against the term
|
||||
getProfileSuggestions(profile) {
|
||||
const profileSuggestions = [];
|
||||
getProfileSuggestions(profile: UserProfile) {
|
||||
const profileSuggestions: string[] = [];
|
||||
if (!profile) {
|
||||
return profileSuggestions;
|
||||
}
|
||||
@ -82,8 +138,8 @@ export default class AtMentionProvider extends Provider {
|
||||
|
||||
// retrieves the parts of the group mention that should be checked
|
||||
// against the term
|
||||
getGroupSuggestions(group) {
|
||||
const groupSuggestions = [];
|
||||
getGroupSuggestions(group: Group) {
|
||||
const groupSuggestions: string[] = [];
|
||||
if (!group) {
|
||||
return groupSuggestions;
|
||||
}
|
||||
@ -101,12 +157,12 @@ export default class AtMentionProvider extends Provider {
|
||||
}
|
||||
|
||||
// normalizeString performs a unicode normalization to a string
|
||||
normalizeString(name) {
|
||||
normalizeString(name: string) {
|
||||
return name.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
// filterProfile constrains profiles to those matching the latest prefix.
|
||||
filterProfile(profile) {
|
||||
filterProfile(profile: UserProfile | UserProfileWithLastViewAt) {
|
||||
if (!profile) {
|
||||
return false;
|
||||
}
|
||||
@ -119,7 +175,7 @@ export default class AtMentionProvider extends Provider {
|
||||
}
|
||||
|
||||
// filterGroup constrains group mentions to those matching the latest prefix.
|
||||
filterGroup(group) {
|
||||
filterGroup(group: Group) {
|
||||
if (!group) {
|
||||
return false;
|
||||
}
|
||||
@ -182,8 +238,8 @@ export default class AtMentionProvider extends Provider {
|
||||
}
|
||||
|
||||
const remoteMembers = (this.data.users || []).
|
||||
filter((profile) => this.filterProfile(profile)).
|
||||
map((profile) => this.createFromProfile(profile, Constants.MENTION_MEMBERS));
|
||||
filter((profile: UserProfileWithLastViewAt) => this.filterProfile(profile)).
|
||||
map((profile: UserProfileWithLastViewAt) => this.createFromProfile(profile, Constants.MENTION_MEMBERS));
|
||||
|
||||
return remoteMembers;
|
||||
}
|
||||
@ -193,30 +249,30 @@ export default class AtMentionProvider extends Provider {
|
||||
if (!this.data) {
|
||||
return [];
|
||||
}
|
||||
const remoteGroups = (this.data.groups || []).
|
||||
filter((group) => this.filterGroup(group)).
|
||||
map((group) => this.createFromGroup(group, Constants.MENTION_GROUPS));
|
||||
const remoteGroups = ((this.data.groups || []) as Group[]).
|
||||
filter((group: Group) => this.filterGroup(group)).
|
||||
map((group: Group) => this.createFromGroup(group, Constants.MENTION_GROUPS));
|
||||
|
||||
return remoteGroups;
|
||||
}
|
||||
|
||||
// remoteNonMembers matches users listed as not in the channel by the server.
|
||||
// listed in the channel from local results.
|
||||
remoteNonMembers() {
|
||||
remoteNonMembers(): CreatedProfile[] {
|
||||
if (!this.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (this.data.out_of_channel || []).
|
||||
filter((profile) => this.filterProfile(profile)).
|
||||
map((profile) => ({
|
||||
filter((profile: UserProfileWithLastViewAt) => this.filterProfile(profile)).
|
||||
map((profile: UserProfileWithLastViewAt) => ({
|
||||
type: Constants.MENTION_NONMEMBERS,
|
||||
...profile,
|
||||
}));
|
||||
}
|
||||
|
||||
items() {
|
||||
const priorityProfilesIds = {};
|
||||
const priorityProfilesIds: Record<string, boolean> = {};
|
||||
const priorityProfiles = this.filterPriorityProfiles();
|
||||
|
||||
priorityProfiles.forEach((member) => {
|
||||
@ -226,16 +282,16 @@ export default class AtMentionProvider extends Provider {
|
||||
const specialMentions = this.specialMentions();
|
||||
const localMembers = this.localMembers().filter((member) => !priorityProfilesIds[member.id]);
|
||||
|
||||
const localUserIds = {};
|
||||
const localUserIds: Record<string, boolean> = {};
|
||||
|
||||
localMembers.forEach((member) => {
|
||||
localUserIds[member.id] = true;
|
||||
});
|
||||
|
||||
const remoteMembers = this.remoteMembers().filter((member) => !localUserIds[member.id] && !priorityProfilesIds[member.id]);
|
||||
const remoteMembers = this.remoteMembers().filter((member: CreatedProfile) => !localUserIds[member.id] && !priorityProfilesIds[member.id]);
|
||||
|
||||
// comparator which prioritises users with usernames starting with search term
|
||||
const orderUsers = (a, b) => {
|
||||
const orderUsers = (a: CreatedProfile, b: CreatedProfile) => {
|
||||
const aStartsWith = a.username.startsWith(this.latestPrefix);
|
||||
const bStartsWith = b.username.startsWith(this.latestPrefix);
|
||||
|
||||
@ -263,7 +319,7 @@ export default class AtMentionProvider extends Provider {
|
||||
// handle groups
|
||||
const localGroups = this.localGroups();
|
||||
|
||||
const localGroupIds = {};
|
||||
const localGroupIds: Record<string, boolean> = {};
|
||||
localGroups.forEach((group) => {
|
||||
localGroupIds[group.id] = true;
|
||||
});
|
||||
@ -271,7 +327,7 @@ export default class AtMentionProvider extends Provider {
|
||||
const remoteGroups = this.remoteGroups().filter((group) => !localGroupIds[group.id]);
|
||||
|
||||
// comparator which prioritises users with usernames starting with search term
|
||||
const orderGroups = (a, b) => {
|
||||
const orderGroups = (a: CreatedGroup, b: CreatedGroup) => {
|
||||
const aStartsWith = a.name.startsWith(this.latestPrefix);
|
||||
const bStartsWith = b.name.startsWith(this.latestPrefix);
|
||||
|
||||
@ -294,11 +350,11 @@ export default class AtMentionProvider extends Provider {
|
||||
filter((member) => !localUserIds[member.id]).
|
||||
sort(orderUsers);
|
||||
|
||||
return priorityProfiles.concat(localAndRemoteMembers).concat(localAndRemoteGroups).concat(specialMentions).concat(remoteNonMembers);
|
||||
return [...priorityProfiles, ...localAndRemoteMembers, ...localAndRemoteGroups, ...specialMentions, ...remoteNonMembers];
|
||||
}
|
||||
|
||||
// updateMatches invokes the resultCallback with the metadata for rendering at mentions
|
||||
updateMatches(resultCallback, items) {
|
||||
updateMatches(resultCallback: ResultsCallback, items: any[]) {
|
||||
if (items.length === 0) {
|
||||
this.lastPrefixWithNoResults = this.latestPrefix;
|
||||
} else if (this.lastPrefixWithNoResults === this.latestPrefix) {
|
||||
@ -321,7 +377,7 @@ export default class AtMentionProvider extends Provider {
|
||||
});
|
||||
}
|
||||
|
||||
handlePretextChanged(pretext, resultCallback) {
|
||||
handlePretextChanged(pretext: string, resultCallback: ResultsCallback) {
|
||||
const captured = regexForAtMention.exec(pretext.toLowerCase());
|
||||
if (!captured) {
|
||||
return false;
|
||||
@ -342,15 +398,15 @@ export default class AtMentionProvider extends Provider {
|
||||
this.updateMatches(resultCallback, this.items());
|
||||
|
||||
// If we haven't gotten server-side results in 500 ms, add the loading indicator.
|
||||
let showLoadingIndicator = setTimeout(() => {
|
||||
let showLoadingIndicator: NodeJS.Timeout | null = setTimeout(() => {
|
||||
if (this.shouldCancelDispatch(prefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateMatches(resultCallback, this.items().concat([{
|
||||
this.updateMatches(resultCallback, [...this.items(), ...[{
|
||||
type: Constants.MENTION_MORE_MEMBERS,
|
||||
loading: true,
|
||||
}]));
|
||||
}]]);
|
||||
|
||||
showLoadingIndicator = null;
|
||||
}, 500);
|
||||
@ -375,12 +431,12 @@ export default class AtMentionProvider extends Provider {
|
||||
return true;
|
||||
}
|
||||
|
||||
handleCompleteWord(term) {
|
||||
handleCompleteWord(term: string) {
|
||||
this.lastCompletedWord = term;
|
||||
this.lastPrefixWithNoResults = '';
|
||||
}
|
||||
|
||||
createFromProfile(profile, type) {
|
||||
createFromProfile(profile: UserProfile | UserProfileWithLastViewAt, type: string): CreatedProfile {
|
||||
if (profile.id === this.currentUserId) {
|
||||
return {
|
||||
type,
|
||||
@ -395,7 +451,7 @@ export default class AtMentionProvider extends Provider {
|
||||
};
|
||||
}
|
||||
|
||||
createFromGroup(group, type) {
|
||||
createFromGroup(group: Group, type: string): CreatedGroup {
|
||||
return {
|
||||
type,
|
||||
...group,
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AtMentionProvider from './at_mention_provider.jsx';
|
||||
import AtMentionProvider from './at_mention_provider';
|
||||
|
||||
export default AtMentionProvider;
|
||||
|
@ -4,7 +4,7 @@
|
||||
import {shallow, mount} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import AtMentionProvider from 'components/suggestion/at_mention_provider/at_mention_provider.jsx';
|
||||
import AtMentionProvider from 'components/suggestion/at_mention_provider/at_mention_provider';
|
||||
import CommandProvider from 'components/suggestion/command_provider/command_provider';
|
||||
import SuggestionBox from 'components/suggestion/suggestion_box/suggestion_box';
|
||||
import SuggestionList from 'components/suggestion/suggestion_list';
|
||||
|
@ -4,22 +4,20 @@
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import Textbox from 'components/textbox/textbox';
|
||||
import Textbox, {type Props} from 'components/textbox/textbox';
|
||||
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
describe('components/TextBox', () => {
|
||||
const baseProps = {
|
||||
const baseProps: Props = {
|
||||
channelId: 'channelId',
|
||||
rootId: 'rootId',
|
||||
currentUserId: 'currentUserId',
|
||||
currentTeamId: 'currentTeamId',
|
||||
profilesInChannel: [
|
||||
{id: 'id1'},
|
||||
{id: 'id2'},
|
||||
],
|
||||
delayChannelAutocomplete: false,
|
||||
autocompleteGroups: [
|
||||
{id: 'gid1'},
|
||||
{id: 'gid2'},
|
||||
TestHelper.getGroupMock({id: 'gid1'}),
|
||||
TestHelper.getGroupMock({id: 'gid2'}),
|
||||
],
|
||||
actions: {
|
||||
autocompleteUsersInChannel: jest.fn(),
|
||||
@ -28,79 +26,89 @@ describe('components/TextBox', () => {
|
||||
},
|
||||
useChannelMentions: true,
|
||||
tabIndex: 0,
|
||||
id: '',
|
||||
value: '',
|
||||
onChange: jest.fn(),
|
||||
onKeyPress: jest.fn(),
|
||||
createMessage: '',
|
||||
characterLimit: 0,
|
||||
};
|
||||
|
||||
test('should match snapshot with required props', () => {
|
||||
function emptyFunction() {} //eslint-disable-line no-empty-function
|
||||
const emptyFunction = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
id: 'someid',
|
||||
value: 'some test text',
|
||||
onChange: emptyFunction,
|
||||
onKeyPress: emptyFunction,
|
||||
characterLimit: 400,
|
||||
createMessage: 'placeholder text',
|
||||
supportsCommands: false,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<Textbox
|
||||
id='someid'
|
||||
value='some test text'
|
||||
onChange={emptyFunction}
|
||||
onKeyPress={emptyFunction}
|
||||
characterLimit={4000}
|
||||
createMessage='placeholder text'
|
||||
supportsCommands={false}
|
||||
{...baseProps}
|
||||
/>,
|
||||
<Textbox {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with additional, optional props', () => {
|
||||
function emptyFunction() {} //eslint-disable-line no-empty-function
|
||||
const emptyFunction = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
id: 'someid',
|
||||
value: 'some test text',
|
||||
onChange: emptyFunction,
|
||||
onKeyPress: emptyFunction,
|
||||
characterLimit: 4000,
|
||||
createMessage: 'placeholder text',
|
||||
supportsCommands: false,
|
||||
rootId: 'root_id',
|
||||
onComposition: jest.fn().mockReturnValue({}),
|
||||
onHeightChange: jest.fn().mockReturnValue({}),
|
||||
onKeyDown: jest.fn().mockReturnValue({}),
|
||||
onMouseUp: jest.fn().mockReturnValue({}),
|
||||
onKeyUp: jest.fn().mockReturnValue({}),
|
||||
onBlur: jest.fn().mockReturnValue({}),
|
||||
handlePostError: jest.fn().mockReturnValue({}),
|
||||
suggestionListPosition: 'top' as const,
|
||||
emojiEnabled: true,
|
||||
disabled: true,
|
||||
badConnection: true,
|
||||
preview: true,
|
||||
openWhenEmpty: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<Textbox
|
||||
id='someid'
|
||||
value='some test text'
|
||||
onChange={emptyFunction}
|
||||
onKeyPress={emptyFunction}
|
||||
characterLimit={4000}
|
||||
createMessage='placeholder text'
|
||||
supportsCommands={false}
|
||||
{...baseProps}
|
||||
rootId='root_id'
|
||||
onComposition={() => {}}
|
||||
onHeightChange={() => {}}
|
||||
onKeyDown={() => {}}
|
||||
onMouseUp={() => {}}
|
||||
onKeyUp={() => {}}
|
||||
onBlur={() => {}}
|
||||
handlePostError={() => {}}
|
||||
suggestionListPosition='top'
|
||||
emojiEnabled={true}
|
||||
disabled={true}
|
||||
badConnection={true}
|
||||
preview={true}
|
||||
openWhenEmpty={true}
|
||||
/>,
|
||||
<Textbox {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should throw error when value is too long', () => {
|
||||
function emptyFunction() {} //eslint-disable-line no-empty-function
|
||||
const emptyFunction = jest.fn();
|
||||
|
||||
// this mock function should be called when the textbox value is too long
|
||||
let gotError = false;
|
||||
function handlePostError(msg: React.ReactNode) {
|
||||
const handlePostError = jest.fn((msg: React.ReactNode) => {
|
||||
gotError = msg !== null;
|
||||
}
|
||||
});
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
id: 'someid',
|
||||
value: 'some test text that exceeds char limit',
|
||||
onChange: emptyFunction,
|
||||
onKeyPress: emptyFunction,
|
||||
characterLimit: 14,
|
||||
createMessage: 'placeholder text',
|
||||
supportsCommands: false,
|
||||
handlePostError,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<Textbox
|
||||
id='someid'
|
||||
value='some test text that exceeds char limit'
|
||||
onChange={emptyFunction}
|
||||
onKeyPress={emptyFunction}
|
||||
characterLimit={14}
|
||||
createMessage='placeholder text'
|
||||
supportsCommands={false}
|
||||
handlePostError={handlePostError}
|
||||
{...baseProps}
|
||||
/>,
|
||||
<Textbox {...props}/>,
|
||||
);
|
||||
|
||||
expect(gotError).toEqual(true);
|
||||
@ -108,26 +116,28 @@ describe('components/TextBox', () => {
|
||||
});
|
||||
|
||||
test('should throw error when new property is too long', () => {
|
||||
function emptyFunction() {} //eslint-disable-line no-empty-function
|
||||
const emptyFunction = jest.fn();
|
||||
|
||||
// this mock function should be called when the textbox value is too long
|
||||
let gotError = false;
|
||||
function handlePostError(msg: React.ReactNode) {
|
||||
const handlePostError = jest.fn((msg: React.ReactNode) => {
|
||||
gotError = msg !== null;
|
||||
}
|
||||
});
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
id: 'someid',
|
||||
value: 'some test text',
|
||||
onChange: emptyFunction,
|
||||
onKeyPress: emptyFunction,
|
||||
characterLimit: 14,
|
||||
createMessage: 'placeholder text',
|
||||
supportsCommands: false,
|
||||
handlePostError,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<Textbox
|
||||
id='someid'
|
||||
value='some test text'
|
||||
onChange={emptyFunction}
|
||||
onKeyPress={emptyFunction}
|
||||
characterLimit={14}
|
||||
createMessage='placeholder text'
|
||||
supportsCommands={false}
|
||||
handlePostError={handlePostError}
|
||||
{...baseProps}
|
||||
/>,
|
||||
<Textbox {...props}/>,
|
||||
);
|
||||
|
||||
wrapper.setProps({value: 'some test text that exceeds char limit'});
|
||||
|
@ -7,6 +7,7 @@ import type {ChangeEvent, ElementType, FocusEvent, KeyboardEvent, MouseEvent} fr
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {Group} from '@mattermost/types/groups';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
@ -59,7 +60,7 @@ export type Props = {
|
||||
currentUserId: string;
|
||||
currentTeamId: string;
|
||||
preview?: boolean;
|
||||
autocompleteGroups: Array<{ id: string }> | null;
|
||||
autocompleteGroups: Group[] | null;
|
||||
delayChannelAutocomplete: boolean;
|
||||
actions: {
|
||||
autocompleteUsersInChannel: (prefix: string, channelId: string) => Promise<ActionResult>;
|
||||
|
@ -41,7 +41,7 @@ import {
|
||||
|
||||
export {getCurrentUser, getCurrentUserId, getUsers};
|
||||
|
||||
type Filters = {
|
||||
export type Filters = {
|
||||
role?: string;
|
||||
inactive?: boolean;
|
||||
active?: boolean;
|
||||
|
Loading…
Reference in New Issue
Block a user