[MM-47181] Migrate "components/suggestion/at_mention_provider" and associated tests to Typescript (#25342)

Этот коммит содержится в:
Sudhanva-Nadiger
2024-01-25 10:58:09 +05:30
коммит произвёл GitHub
родитель 8f0abc1316
Коммит 53298a2d60
8 изменённых файлов: 306 добавлений и 141 удалений

Просмотреть файл

@@ -8,7 +8,7 @@ exports[`components/TextBox should match snapshot with additional, optional prop
className="form-control custom-textarea textbox-preview-area" className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]} onBlur={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
onKeyPress={[Function]} onKeyPress={[MockFunction]}
tabIndex={0} tabIndex={0}
> >
<Connect(PostMarkdown) <Connect(PostMarkdown)
@@ -52,10 +52,10 @@ exports[`components/TextBox should match snapshot with additional, optional prop
listPosition="top" listPosition="top"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
onComposition={[Function]} onComposition={[MockFunction]}
onHeightChange={[Function]} onHeightChange={[MockFunction]}
onKeyDown={[Function]} onKeyDown={[Function]}
onKeyPress={[Function]} onKeyPress={[MockFunction]}
onKeyUp={[Function]} onKeyUp={[Function]}
onMouseUp={[Function]} onMouseUp={[Function]}
openWhenEmpty={true} openWhenEmpty={true}
@@ -66,10 +66,34 @@ exports[`components/TextBox should match snapshot with additional, optional prop
"addLastViewAtToProfiles": [Function], "addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [ "autocompleteGroups": Array [
Object { Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1", "id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
}, },
Object { Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2", "id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
}, },
], ],
"autocompleteUsersInChannel": [Function], "autocompleteUsersInChannel": [Function],
@@ -136,7 +160,7 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
className="form-control custom-textarea textbox-preview-area" className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]} onBlur={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
onKeyPress={[Function]} onKeyPress={[MockFunction]}
tabIndex={0} tabIndex={0}
> >
<Connect(PostMarkdown) <Connect(PostMarkdown)
@@ -179,7 +203,7 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
onKeyPress={[Function]} onKeyPress={[MockFunction]}
onKeyUp={[Function]} onKeyUp={[Function]}
onMouseUp={[Function]} onMouseUp={[Function]}
placeholder="placeholder text" placeholder="placeholder text"
@@ -189,10 +213,34 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
"addLastViewAtToProfiles": [Function], "addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [ "autocompleteGroups": Array [
Object { Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1", "id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
}, },
Object { Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2", "id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
}, },
], ],
"autocompleteUsersInChannel": [Function], "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" className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]} onBlur={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
onKeyPress={[Function]} onKeyPress={[MockFunction]}
tabIndex={0} tabIndex={0}
> >
<Connect(PostMarkdown) <Connect(PostMarkdown)
@@ -302,7 +350,7 @@ exports[`components/TextBox should throw error when new property is too long 1`]
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
onKeyPress={[Function]} onKeyPress={[MockFunction]}
onKeyUp={[Function]} onKeyUp={[Function]}
onMouseUp={[Function]} onMouseUp={[Function]}
placeholder="placeholder text" placeholder="placeholder text"
@@ -312,10 +360,34 @@ exports[`components/TextBox should throw error when new property is too long 1`]
"addLastViewAtToProfiles": [Function], "addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [ "autocompleteGroups": Array [
Object { Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1", "id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
}, },
Object { Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2", "id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
}, },
], ],
"autocompleteUsersInChannel": [Function], "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" className="form-control custom-textarea textbox-preview-area"
onBlur={[Function]} onBlur={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
onKeyPress={[Function]} onKeyPress={[MockFunction]}
tabIndex={0} tabIndex={0}
> >
<Connect(PostMarkdown) <Connect(PostMarkdown)
@@ -425,7 +497,7 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
onKeyPress={[Function]} onKeyPress={[MockFunction]}
onKeyUp={[Function]} onKeyUp={[Function]}
onMouseUp={[Function]} onMouseUp={[Function]}
placeholder="placeholder text" placeholder="placeholder text"
@@ -435,10 +507,34 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
"addLastViewAtToProfiles": [Function], "addLastViewAtToProfiles": [Function],
"autocompleteGroups": Array [ "autocompleteGroups": Array [
Object { Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid1", "id": "gid1",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
}, },
Object { Object {
"allow_reference": true,
"create_at": 1,
"delete_at": 0,
"description": "",
"display_name": "group_display_name",
"has_syncables": false,
"id": "gid2", "id": "gid2",
"member_count": 0,
"name": "group_name",
"remote_id": "",
"scheme_admin": false,
"source": "",
"update_at": 1,
}, },
], ],
"autocompleteUsersInChannel": [Function], "autocompleteUsersInChannel": [Function],

Просмотреть файл

@@ -1,35 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // 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 AtMentionSuggestion from 'components/suggestion/at_mention_provider/at_mention_suggestion';
import {Constants} from 'utils/constants'; import {Constants} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
jest.useFakeTimers(); jest.useFakeTimers();
describe('components/suggestion/at_mention_provider/AtMentionProvider', () => { describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
const userid10 = {id: 'userid10', username: 'nicknamer', first_name: '', last_name: '', nickname: 'Z'}; const userid10 = TestHelper.getUserMock({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 userid3 = TestHelper.getUserMock({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 userid1 = {...TestHelper.getUserMock({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 userid2 = TestHelper.getUserMock({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 userid4 = TestHelper.getUserMock({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 userid5 = TestHelper.getUserMock({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 userid6 = TestHelper.getUserMock({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 userid7 = TestHelper.getUserMock({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 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 groupid1 = TestHelper.getGroupMock({id: 'groupid1', name: 'board', display_name: 'board'});
const groupid2 = {id: 'groupid2', name: 'developers', display_name: 'developers'}; const groupid2 = TestHelper.getGroupMock({id: 'groupid2', name: 'developers', display_name: 'developers'});
const groupid3 = {id: 'groupid3', name: 'software-engineers', display_name: 'software engineers'}; const groupid3 = TestHelper.getGroupMock({id: 'groupid3', name: 'software-engineers', display_name: 'software engineers'});
const baseParams = { const baseParams: Props = {
currentUserId: 'userid1', currentUserId: 'userid1',
channelId: 'channelid1', channelId: 'channelid1',
autocompleteUsersInChannel: jest.fn().mockResolvedValue(false), autocompleteUsersInChannel: jest.fn().mockResolvedValue(false),
autocompleteGroups: [groupid1, groupid2, groupid3], autocompleteGroups: [groupid1, groupid2, groupid3],
useChannelMentions: true, useChannelMentions: true,
searchAssociatedGroupsForReference: jest.fn().mockResolvedValue(false), searchAssociatedGroupsForReference: jest.fn().mockResolvedValue(false),
priorityProfiles: [],
}; };
it('should ignore pretexts that are not at-mentions', () => { 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 () => { it('should have priorityProfiles at the top', async () => {
const userid11 = {id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'}; const userid11 = TestHelper.getUserMock({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 userid12 = TestHelper.getUserMock({id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'});
const pretext = '@'; const pretext = '@';
const matchedPretext = '@'; const matchedPretext = '@';
@@ -315,8 +317,8 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
}); });
it('should remove duplicates from results', async () => { it('should remove duplicates from results', async () => {
const userid11 = {id: 'userid11', username: 'user11', first_name: 'firstname11', last_name: 'lastname11', nickname: 'nickname11'}; const userid11 = TestHelper.getUserMock({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 userid12 = TestHelper.getUserMock({id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'});
const pretext = '@'; const pretext = '@';
const matchedPretext = '@'; const matchedPretext = '@';
@@ -461,8 +463,8 @@ describe('components/suggestion/at_mention_provider/AtMentionProvider', () => {
}); });
it('should sort results based on last_viewed_at', async () => { 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 userid11 = TestHelper.getUserMock({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 userid12 = TestHelper.getUserMock({id: 'userid12', username: 'user12', first_name: 'firstname12', last_name: 'lastname12', nickname: 'nickname12'});
const pretext = '@'; const pretext = '@';
const matchedPretext = '@'; const matchedPretext = '@';

Просмотреть файл

@@ -1,14 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // 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 {makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
import {makeAddLastViewAtToProfiles} from 'mattermost-redux/selectors/entities/utils'; 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 {getSuggestionsSplitBy, getSuggestionsSplitByMultiple} from 'mattermost-redux/utils/user_utils';
import store from 'stores/redux_store'; import store from 'stores/redux_store';
import {Constants} from 'utils/constants'; import {Constants} from 'utils/constants';
import type {GlobalState} from 'types/store';
import AtMentionSuggestion from './at_mention_suggestion'; import AtMentionSuggestion from './at_mention_suggestion';
import Provider from '../provider'; import Provider from '../provider';
@@ -16,14 +23,65 @@ import Provider from '../provider';
const profilesInChannelOptions = {active: true}; const profilesInChannelOptions = {active: true};
const regexForAtMention = /(?:^|\W)@([\p{L}\d\-_. ]*)$/iu; 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, // 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 // users in the channel and users not in the channel. It mixes together results from the local
// store with results fetched from the server. // store with results fetched from the server.
export default class AtMentionProvider extends Provider { 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(); 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.data = null;
this.lastCompletedWord = ''; this.lastCompletedWord = '';
@@ -33,9 +91,7 @@ export default class AtMentionProvider extends Provider {
this.addLastViewAtToProfiles = makeAddLastViewAtToProfiles(); this.addLastViewAtToProfiles = makeAddLastViewAtToProfiles();
} }
// setProps gives the provider additional context for matching pretexts. Ideally this would setProps({currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles}: Props) {
// just be something akin to a connected component with access to the store itself.
setProps({currentUserId, channelId, autocompleteUsersInChannel, useChannelMentions, autocompleteGroups, searchAssociatedGroupsForReference, priorityProfiles}) {
this.currentUserId = currentUserId; this.currentUserId = currentUserId;
this.channelId = channelId; this.channelId = channelId;
this.autocompleteUsersInChannel = autocompleteUsersInChannel; this.autocompleteUsersInChannel = autocompleteUsersInChannel;
@@ -61,8 +117,8 @@ export default class AtMentionProvider extends Provider {
// retrieves the parts of the profile that should be checked // retrieves the parts of the profile that should be checked
// against the term // against the term
getProfileSuggestions(profile) { getProfileSuggestions(profile: UserProfile) {
const profileSuggestions = []; const profileSuggestions: string[] = [];
if (!profile) { if (!profile) {
return profileSuggestions; return profileSuggestions;
} }
@@ -82,8 +138,8 @@ export default class AtMentionProvider extends Provider {
// retrieves the parts of the group mention that should be checked // retrieves the parts of the group mention that should be checked
// against the term // against the term
getGroupSuggestions(group) { getGroupSuggestions(group: Group) {
const groupSuggestions = []; const groupSuggestions: string[] = [];
if (!group) { if (!group) {
return groupSuggestions; return groupSuggestions;
} }
@@ -101,12 +157,12 @@ export default class AtMentionProvider extends Provider {
} }
// normalizeString performs a unicode normalization to a string // normalizeString performs a unicode normalization to a string
normalizeString(name) { normalizeString(name: string) {
return name.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); return name.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
} }
// filterProfile constrains profiles to those matching the latest prefix. // filterProfile constrains profiles to those matching the latest prefix.
filterProfile(profile) { filterProfile(profile: UserProfile | UserProfileWithLastViewAt) {
if (!profile) { if (!profile) {
return false; return false;
} }
@@ -119,7 +175,7 @@ export default class AtMentionProvider extends Provider {
} }
// filterGroup constrains group mentions to those matching the latest prefix. // filterGroup constrains group mentions to those matching the latest prefix.
filterGroup(group) { filterGroup(group: Group) {
if (!group) { if (!group) {
return false; return false;
} }
@@ -182,8 +238,8 @@ export default class AtMentionProvider extends Provider {
} }
const remoteMembers = (this.data.users || []). const remoteMembers = (this.data.users || []).
filter((profile) => this.filterProfile(profile)). filter((profile: UserProfileWithLastViewAt) => this.filterProfile(profile)).
map((profile) => this.createFromProfile(profile, Constants.MENTION_MEMBERS)); map((profile: UserProfileWithLastViewAt) => this.createFromProfile(profile, Constants.MENTION_MEMBERS));
return remoteMembers; return remoteMembers;
} }
@@ -193,30 +249,30 @@ export default class AtMentionProvider extends Provider {
if (!this.data) { if (!this.data) {
return []; return [];
} }
const remoteGroups = (this.data.groups || []). const remoteGroups = ((this.data.groups || []) as Group[]).
filter((group) => this.filterGroup(group)). filter((group: Group) => this.filterGroup(group)).
map((group) => this.createFromGroup(group, Constants.MENTION_GROUPS)); map((group: Group) => this.createFromGroup(group, Constants.MENTION_GROUPS));
return remoteGroups; return remoteGroups;
} }
// remoteNonMembers matches users listed as not in the channel by the server. // remoteNonMembers matches users listed as not in the channel by the server.
// listed in the channel from local results. // listed in the channel from local results.
remoteNonMembers() { remoteNonMembers(): CreatedProfile[] {
if (!this.data) { if (!this.data) {
return []; return [];
} }
return (this.data.out_of_channel || []). return (this.data.out_of_channel || []).
filter((profile) => this.filterProfile(profile)). filter((profile: UserProfileWithLastViewAt) => this.filterProfile(profile)).
map((profile) => ({ map((profile: UserProfileWithLastViewAt) => ({
type: Constants.MENTION_NONMEMBERS, type: Constants.MENTION_NONMEMBERS,
...profile, ...profile,
})); }));
} }
items() { items() {
const priorityProfilesIds = {}; const priorityProfilesIds: Record<string, boolean> = {};
const priorityProfiles = this.filterPriorityProfiles(); const priorityProfiles = this.filterPriorityProfiles();
priorityProfiles.forEach((member) => { priorityProfiles.forEach((member) => {
@@ -226,16 +282,16 @@ export default class AtMentionProvider extends Provider {
const specialMentions = this.specialMentions(); const specialMentions = this.specialMentions();
const localMembers = this.localMembers().filter((member) => !priorityProfilesIds[member.id]); const localMembers = this.localMembers().filter((member) => !priorityProfilesIds[member.id]);
const localUserIds = {}; const localUserIds: Record<string, boolean> = {};
localMembers.forEach((member) => { localMembers.forEach((member) => {
localUserIds[member.id] = true; 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 // 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 aStartsWith = a.username.startsWith(this.latestPrefix);
const bStartsWith = b.username.startsWith(this.latestPrefix); const bStartsWith = b.username.startsWith(this.latestPrefix);
@@ -263,7 +319,7 @@ export default class AtMentionProvider extends Provider {
// handle groups // handle groups
const localGroups = this.localGroups(); const localGroups = this.localGroups();
const localGroupIds = {}; const localGroupIds: Record<string, boolean> = {};
localGroups.forEach((group) => { localGroups.forEach((group) => {
localGroupIds[group.id] = true; localGroupIds[group.id] = true;
}); });
@@ -271,7 +327,7 @@ export default class AtMentionProvider extends Provider {
const remoteGroups = this.remoteGroups().filter((group) => !localGroupIds[group.id]); const remoteGroups = this.remoteGroups().filter((group) => !localGroupIds[group.id]);
// comparator which prioritises users with usernames starting with search term // 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 aStartsWith = a.name.startsWith(this.latestPrefix);
const bStartsWith = b.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]). filter((member) => !localUserIds[member.id]).
sort(orderUsers); 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 invokes the resultCallback with the metadata for rendering at mentions
updateMatches(resultCallback, items) { updateMatches(resultCallback: ResultsCallback, items: any[]) {
if (items.length === 0) { if (items.length === 0) {
this.lastPrefixWithNoResults = this.latestPrefix; this.lastPrefixWithNoResults = this.latestPrefix;
} else if (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()); const captured = regexForAtMention.exec(pretext.toLowerCase());
if (!captured) { if (!captured) {
return false; return false;
@@ -342,15 +398,15 @@ export default class AtMentionProvider extends Provider {
this.updateMatches(resultCallback, this.items()); this.updateMatches(resultCallback, this.items());
// If we haven't gotten server-side results in 500 ms, add the loading indicator. // 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)) { if (this.shouldCancelDispatch(prefix)) {
return; return;
} }
this.updateMatches(resultCallback, this.items().concat([{ this.updateMatches(resultCallback, [...this.items(), ...[{
type: Constants.MENTION_MORE_MEMBERS, type: Constants.MENTION_MORE_MEMBERS,
loading: true, loading: true,
}])); }]]);
showLoadingIndicator = null; showLoadingIndicator = null;
}, 500); }, 500);
@@ -375,12 +431,12 @@ export default class AtMentionProvider extends Provider {
return true; return true;
} }
handleCompleteWord(term) { handleCompleteWord(term: string) {
this.lastCompletedWord = term; this.lastCompletedWord = term;
this.lastPrefixWithNoResults = ''; this.lastPrefixWithNoResults = '';
} }
createFromProfile(profile, type) { createFromProfile(profile: UserProfile | UserProfileWithLastViewAt, type: string): CreatedProfile {
if (profile.id === this.currentUserId) { if (profile.id === this.currentUserId) {
return { return {
type, type,
@@ -395,7 +451,7 @@ export default class AtMentionProvider extends Provider {
}; };
} }
createFromGroup(group, type) { createFromGroup(group: Group, type: string): CreatedGroup {
return { return {
type, type,
...group, ...group,

Просмотреть файл

@@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import AtMentionProvider from './at_mention_provider.jsx'; import AtMentionProvider from './at_mention_provider';
export default AtMentionProvider; export default AtMentionProvider;

Просмотреть файл

@@ -4,7 +4,7 @@
import {shallow, mount} from 'enzyme'; import {shallow, mount} from 'enzyme';
import React from 'react'; 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 CommandProvider from 'components/suggestion/command_provider/command_provider';
import SuggestionBox from 'components/suggestion/suggestion_box/suggestion_box'; import SuggestionBox from 'components/suggestion/suggestion_box/suggestion_box';
import SuggestionList from 'components/suggestion/suggestion_list'; import SuggestionList from 'components/suggestion/suggestion_list';

Просмотреть файл

@@ -4,22 +4,20 @@
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import React from 'react'; 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', () => { describe('components/TextBox', () => {
const baseProps = { const baseProps: Props = {
channelId: 'channelId', channelId: 'channelId',
rootId: 'rootId', rootId: 'rootId',
currentUserId: 'currentUserId', currentUserId: 'currentUserId',
currentTeamId: 'currentTeamId', currentTeamId: 'currentTeamId',
profilesInChannel: [
{id: 'id1'},
{id: 'id2'},
],
delayChannelAutocomplete: false, delayChannelAutocomplete: false,
autocompleteGroups: [ autocompleteGroups: [
{id: 'gid1'}, TestHelper.getGroupMock({id: 'gid1'}),
{id: 'gid2'}, TestHelper.getGroupMock({id: 'gid2'}),
], ],
actions: { actions: {
autocompleteUsersInChannel: jest.fn(), autocompleteUsersInChannel: jest.fn(),
@@ -28,79 +26,89 @@ describe('components/TextBox', () => {
}, },
useChannelMentions: true, useChannelMentions: true,
tabIndex: 0, tabIndex: 0,
id: '',
value: '',
onChange: jest.fn(),
onKeyPress: jest.fn(),
createMessage: '',
characterLimit: 0,
}; };
test('should match snapshot with required props', () => { 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( const wrapper = shallow(
<Textbox <Textbox {...props}/>,
id='someid'
value='some test text'
onChange={emptyFunction}
onKeyPress={emptyFunction}
characterLimit={4000}
createMessage='placeholder text'
supportsCommands={false}
{...baseProps}
/>,
); );
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
test('should match snapshot with additional, optional props', () => { 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( const wrapper = shallow(
<Textbox <Textbox {...props}/>,
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}
/>,
); );
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
test('should throw error when value is too long', () => { 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 // this mock function should be called when the textbox value is too long
let gotError = false; let gotError = false;
function handlePostError(msg: React.ReactNode) { const handlePostError = jest.fn((msg: React.ReactNode) => {
gotError = msg !== null; 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( const wrapper = shallow(
<Textbox <Textbox {...props}/>,
id='someid'
value='some test text that exceeds char limit'
onChange={emptyFunction}
onKeyPress={emptyFunction}
characterLimit={14}
createMessage='placeholder text'
supportsCommands={false}
handlePostError={handlePostError}
{...baseProps}
/>,
); );
expect(gotError).toEqual(true); expect(gotError).toEqual(true);
@@ -108,26 +116,28 @@ describe('components/TextBox', () => {
}); });
test('should throw error when new property is too long', () => { 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 // this mock function should be called when the textbox value is too long
let gotError = false; let gotError = false;
function handlePostError(msg: React.ReactNode) { const handlePostError = jest.fn((msg: React.ReactNode) => {
gotError = msg !== null; 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( const wrapper = shallow(
<Textbox <Textbox {...props}/>,
id='someid'
value='some test text'
onChange={emptyFunction}
onKeyPress={emptyFunction}
characterLimit={14}
createMessage='placeholder text'
supportsCommands={false}
handlePostError={handlePostError}
{...baseProps}
/>,
); );
wrapper.setProps({value: 'some test text that exceeds char limit'}); 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 {FormattedMessage} from 'react-intl';
import type {Channel} from '@mattermost/types/channels'; import type {Channel} from '@mattermost/types/channels';
import type {Group} from '@mattermost/types/groups';
import type {UserProfile} from '@mattermost/types/users'; import type {UserProfile} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions'; import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -59,7 +60,7 @@ export type Props = {
currentUserId: string; currentUserId: string;
currentTeamId: string; currentTeamId: string;
preview?: boolean; preview?: boolean;
autocompleteGroups: Array<{ id: string }> | null; autocompleteGroups: Group[] | null;
delayChannelAutocomplete: boolean; delayChannelAutocomplete: boolean;
actions: { actions: {
autocompleteUsersInChannel: (prefix: string, channelId: string) => Promise<ActionResult>; autocompleteUsersInChannel: (prefix: string, channelId: string) => Promise<ActionResult>;

Просмотреть файл

@@ -41,7 +41,7 @@ import {
export {getCurrentUser, getCurrentUserId, getUsers}; export {getCurrentUser, getCurrentUserId, getUsers};
type Filters = { export type Filters = {
role?: string; role?: string;
inactive?: boolean; inactive?: boolean;
active?: boolean; active?: boolean;