Add fetchMissingUsers option to AtMention and messageHtmlToComponent (#29000)
* Add fetchMissingUsers option to AtMention and messageHtmlToComponent * Update snapshots
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ee20ac8f69
Коммит
e1b0f1d70e
13
webapp/channels/src/components/at_mention/actions.ts
Обычный файл
13
webapp/channels/src/components/at_mention/actions.ts
Обычный файл
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {getMissingProfilesByUsernames} from 'mattermost-redux/actions/users';
|
||||||
|
import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
|
import {getPotentialMentionsForName} from 'utils/post_utils';
|
||||||
|
|
||||||
|
export function getMissingMentionedUsers(text: string): ActionFuncAsync<Array<UserProfile['username']>> {
|
||||||
|
return getMissingProfilesByUsernames(getPotentialMentionsForName(text));
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import {General} from 'mattermost-redux/constants';
|
|||||||
|
|
||||||
import AtMention from 'components/at_mention/at_mention';
|
import AtMention from 'components/at_mention/at_mention';
|
||||||
|
|
||||||
|
import {render} from 'tests/react_testing_utils';
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
@@ -28,7 +29,7 @@ describe('components/AtMention', () => {
|
|||||||
marketing: TestHelper.getGroupMock({id: 'qwerty2', name: 'marketing', allow_reference: false}),
|
marketing: TestHelper.getGroupMock({id: 'qwerty2', name: 'marketing', allow_reference: false}),
|
||||||
accounting: TestHelper.getGroupMock({id: 'qwerty3', name: 'accounting', allow_reference: true}),
|
accounting: TestHelper.getGroupMock({id: 'qwerty3', name: 'accounting', allow_reference: true}),
|
||||||
},
|
},
|
||||||
dispatch: jest.fn(),
|
getMissingMentionedUsers: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot when mentioning user', () => {
|
test('should match snapshot when mentioning user', () => {
|
||||||
@@ -227,4 +228,34 @@ describe('components/AtMention', () => {
|
|||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('fetchMissingUsers', () => {
|
||||||
|
test('when fetchMissingUsers is true, should fetch an unloaded user on mount', () => {
|
||||||
|
render(
|
||||||
|
<AtMention
|
||||||
|
{...baseProps}
|
||||||
|
mentionName='someuser'
|
||||||
|
fetchMissingUsers={true}
|
||||||
|
>
|
||||||
|
{'@someuser'}
|
||||||
|
</AtMention>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(baseProps.getMissingMentionedUsers).toHaveBeenCalledWith('someuser');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when fetchMissingUsers is false, should not fetch an unloaded user on mount', () => {
|
||||||
|
shallow(
|
||||||
|
<AtMention
|
||||||
|
{...baseProps}
|
||||||
|
mentionName='someuser'
|
||||||
|
fetchMissingUsers={false}
|
||||||
|
>
|
||||||
|
{'@someuser'}
|
||||||
|
</AtMention>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(baseProps.getMissingMentionedUsers).not.toHaveBeenCalledWith('someuser');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, {useRef, useMemo, memo} from 'react';
|
import React, {useRef, useMemo, memo, useEffect} from 'react';
|
||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||||
@@ -22,6 +22,7 @@ type OwnProps = {
|
|||||||
channelId?: string;
|
channelId?: string;
|
||||||
disableHighlight?: boolean;
|
disableHighlight?: boolean;
|
||||||
disableGroupHighlight?: boolean;
|
disableGroupHighlight?: boolean;
|
||||||
|
fetchMissingUsers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = OwnProps & PropsFromRedux;
|
type Props = OwnProps & PropsFromRedux;
|
||||||
@@ -34,6 +35,12 @@ const AtMention = (props: Props) => {
|
|||||||
[props.mentionName, props.usersByUsername, props.groupsByName, props.disableGroupHighlight],
|
[props.mentionName, props.usersByUsername, props.groupsByName, props.disableGroupHighlight],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user && !group && props.fetchMissingUsers) {
|
||||||
|
props.getMissingMentionedUsers(props.mentionName);
|
||||||
|
}
|
||||||
|
}, [props.mentionName]);
|
||||||
|
|
||||||
const returnFocus = () => {
|
const returnFocus = () => {
|
||||||
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
document.dispatchEvent(new CustomEvent<A11yFocusEventDetail>(
|
||||||
A11yCustomEventTypes.FOCUS, {
|
A11yCustomEventTypes.FOCUS, {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {getCurrentUserId, getUsersByUsername} from 'mattermost-redux/selectors/e
|
|||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
|
import {getMissingMentionedUsers} from './actions';
|
||||||
import AtMention from './at_mention';
|
import AtMention from './at_mention';
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
@@ -21,7 +22,11 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = connect(mapStateToProps);
|
const mapDispatchToProps = {
|
||||||
|
getMissingMentionedUsers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
export default connector(AtMention);
|
export default connector(AtMention);
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
|
|||||||
>
|
>
|
||||||
<Memo(AtMention)
|
<Memo(AtMention)
|
||||||
currentUserId="admin1"
|
currentUserId="admin1"
|
||||||
dispatch={[Function]}
|
getMissingMentionedUsers={[Function]}
|
||||||
groupsByName={Object {}}
|
groupsByName={Object {}}
|
||||||
mentionName="user-0"
|
mentionName="user-0"
|
||||||
teammateNameDisplay="username"
|
teammateNameDisplay="username"
|
||||||
@@ -153,7 +153,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
|
|||||||
>
|
>
|
||||||
<Memo(AtMention)
|
<Memo(AtMention)
|
||||||
currentUserId="admin1"
|
currentUserId="admin1"
|
||||||
dispatch={[Function]}
|
getMissingMentionedUsers={[Function]}
|
||||||
groupsByName={Object {}}
|
groupsByName={Object {}}
|
||||||
mentionName="user-1"
|
mentionName="user-1"
|
||||||
teammateNameDisplay="username"
|
teammateNameDisplay="username"
|
||||||
@@ -316,7 +316,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
|
|||||||
>
|
>
|
||||||
<Memo(AtMention)
|
<Memo(AtMention)
|
||||||
currentUserId="admin1"
|
currentUserId="admin1"
|
||||||
dispatch={[Function]}
|
getMissingMentionedUsers={[Function]}
|
||||||
groupsByName={Object {}}
|
groupsByName={Object {}}
|
||||||
mentionName="user-0"
|
mentionName="user-0"
|
||||||
teammateNameDisplay="username"
|
teammateNameDisplay="username"
|
||||||
@@ -330,7 +330,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
|
|||||||
>
|
>
|
||||||
<Memo(AtMention)
|
<Memo(AtMention)
|
||||||
currentUserId="admin1"
|
currentUserId="admin1"
|
||||||
dispatch={[Function]}
|
getMissingMentionedUsers={[Function]}
|
||||||
groupsByName={Object {}}
|
groupsByName={Object {}}
|
||||||
mentionName="user-1"
|
mentionName="user-1"
|
||||||
teammateNameDisplay="username"
|
teammateNameDisplay="username"
|
||||||
@@ -553,7 +553,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
|
|||||||
>
|
>
|
||||||
<Memo(AtMention)
|
<Memo(AtMention)
|
||||||
currentUserId="admin1"
|
currentUserId="admin1"
|
||||||
dispatch={[Function]}
|
getMissingMentionedUsers={[Function]}
|
||||||
groupsByName={Object {}}
|
groupsByName={Object {}}
|
||||||
mentionName="user-0"
|
mentionName="user-0"
|
||||||
teammateNameDisplay="username"
|
teammateNameDisplay="username"
|
||||||
@@ -873,7 +873,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
|
|||||||
>
|
>
|
||||||
<Memo(AtMention)
|
<Memo(AtMention)
|
||||||
currentUserId="admin1"
|
currentUserId="admin1"
|
||||||
dispatch={[Function]}
|
getMissingMentionedUsers={[Function]}
|
||||||
groupsByName={Object {}}
|
groupsByName={Object {}}
|
||||||
mentionName="user-0"
|
mentionName="user-0"
|
||||||
teammateNameDisplay="username"
|
teammateNameDisplay="username"
|
||||||
|
|||||||
@@ -197,8 +197,8 @@ export function getMissingProfilesByIds(userIds: string[]): ActionFuncAsync<Arra
|
|||||||
|
|
||||||
export function getMissingProfilesByUsernames(usernames: string[]): ActionFuncAsync<Array<UserProfile['username']>> {
|
export function getMissingProfilesByUsernames(usernames: string[]): ActionFuncAsync<Array<UserProfile['username']>> {
|
||||||
return async (dispatch, getState, {loaders}: any) => {
|
return async (dispatch, getState, {loaders}: any) => {
|
||||||
if (!loaders.missingUsernameLoader) {
|
if (!loaders.userByUsernameLoader) {
|
||||||
loaders.missingUsernameLoader = new DelayedDataLoader<UserProfile['username']>({
|
loaders.userByUsernameLoader = new DelayedDataLoader<UserProfile['username']>({
|
||||||
fetchBatch: (usernames) => dispatch(getProfilesByUsernames(usernames)),
|
fetchBatch: (usernames) => dispatch(getProfilesByUsernames(usernames)),
|
||||||
maxBatchSize: maxUserIdsPerProfilesRequest,
|
maxBatchSize: maxUserIdsPerProfilesRequest,
|
||||||
wait: missingProfilesWait,
|
wait: missingProfilesWait,
|
||||||
@@ -210,7 +210,7 @@ export function getMissingProfilesByUsernames(usernames: string[]): ActionFuncAs
|
|||||||
const missingUsernames = usernames.filter((username) => !usersByUsername[username]);
|
const missingUsernames = usernames.filter((username) => !usersByUsername[username]);
|
||||||
|
|
||||||
if (missingUsernames.length > 0) {
|
if (missingUsernames.length > 0) {
|
||||||
await loaders.missingUsernameLoader.queueAndWait(missingUsernames);
|
await loaders.userByUsernameLoader.queueAndWait(missingUsernames);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {data: missingUsernames};
|
return {data: missingUsernames};
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ export type Options = Partial<{
|
|||||||
images: boolean;
|
images: boolean;
|
||||||
atPlanMentions: boolean;
|
atPlanMentions: boolean;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the AtMention component should attempt to fetch at-mentioned users if none can be found for
|
||||||
|
* something that looks like an at-mention. This defaults to false because the web app currently loads at-mentioned
|
||||||
|
* users automatically for all posts.
|
||||||
|
*/
|
||||||
|
fetchMissingUsers: boolean;
|
||||||
}>
|
}>
|
||||||
|
|
||||||
type ProcessingInstruction = {
|
type ProcessingInstruction = {
|
||||||
@@ -132,6 +139,7 @@ export function messageHtmlToComponent(html: string, options: Options = {}) {
|
|||||||
disableHighlight={!mentionHighlight}
|
disableHighlight={!mentionHighlight}
|
||||||
disableGroupHighlight={disableGroupHighlight}
|
disableGroupHighlight={disableGroupHighlight}
|
||||||
channelId={options.channelId}
|
channelId={options.channelId}
|
||||||
|
fetchMissingUsers={options.fetchMissingUsers}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AtMention>
|
</AtMention>
|
||||||
|
|||||||
@@ -757,19 +757,32 @@ export function makeGetIsReactionAlreadyAddedToPost(): (state: GlobalState, post
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMentionDetails(usersByUsername: Record<string, UserProfile | Group>, mentionName: string): UserProfile | Group | undefined {
|
/**
|
||||||
|
* Given the text of an at-mention without the @, returns an array containing that text and every substring of it that
|
||||||
|
* can be made by removing trailing punctiuation.
|
||||||
|
*
|
||||||
|
* For example, getPotentialMentionsForName('username') returns ['username'] and
|
||||||
|
* getPotentialMentionsForName('username..') return ['username..', 'username.', 'username'].
|
||||||
|
*/
|
||||||
|
export function getPotentialMentionsForName(mentionName: string): string[] {
|
||||||
let mentionNameToLowerCase = mentionName.toLowerCase();
|
let mentionNameToLowerCase = mentionName.toLowerCase();
|
||||||
|
|
||||||
while (mentionNameToLowerCase.length > 0) {
|
const potentialMentions = [mentionNameToLowerCase];
|
||||||
if (usersByUsername.hasOwnProperty(mentionNameToLowerCase)) {
|
|
||||||
return usersByUsername[mentionNameToLowerCase];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
|
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
|
||||||
if ((/[._-]$/).test(mentionNameToLowerCase)) {
|
while (mentionNameToLowerCase.length > 0 && (/[._-]$/).test(mentionNameToLowerCase)) {
|
||||||
mentionNameToLowerCase = mentionNameToLowerCase.substring(0, mentionNameToLowerCase.length - 1);
|
mentionNameToLowerCase = mentionNameToLowerCase.substring(0, mentionNameToLowerCase.length - 1);
|
||||||
} else {
|
|
||||||
break;
|
potentialMentions.push(mentionNameToLowerCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return potentialMentions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMentionDetails<T extends UserProfile | Group>(entitiesByName: Record<string, T>, mentionName: string): T | undefined {
|
||||||
|
for (const potentialMention of getPotentialMentionsForName(mentionName)) {
|
||||||
|
if (Object.hasOwn(entitiesByName, potentialMention)) {
|
||||||
|
return entitiesByName[potentialMention];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -783,11 +796,11 @@ export function getUserOrGroupFromMentionName(
|
|||||||
groupsDisabled?: boolean,
|
groupsDisabled?: boolean,
|
||||||
getMention = getMentionDetails,
|
getMention = getMentionDetails,
|
||||||
): [UserProfile?, Group?] {
|
): [UserProfile?, Group?] {
|
||||||
const user = getMention(users, mentionName) as UserProfile | undefined;
|
const user = getMention(users, mentionName);
|
||||||
|
|
||||||
// prioritizes user if user exists with the same name as a group.
|
// prioritizes user if user exists with the same name as a group.
|
||||||
if (!user && !groupsDisabled) {
|
if (!user && !groupsDisabled) {
|
||||||
const group = getMention(groups, mentionName) as Group | undefined;
|
const group = getMention(groups, mentionName);
|
||||||
if (group && !group.allow_reference) {
|
if (group && !group.allow_reference) {
|
||||||
return [undefined, undefined]; // remove group mention if not allowed to reference
|
return [undefined, undefined]; // remove group mention if not allowed to reference
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user