diff --git a/src/common/BaseAnnotator.ts b/src/common/BaseAnnotator.ts index 4e6f4159b..d04700b0e 100644 --- a/src/common/BaseAnnotator.ts +++ b/src/common/BaseAnnotator.ts @@ -152,7 +152,6 @@ export default class BaseAnnotator extends EventEmitter { protected hydrate(): void { // Redux dispatch method signature doesn't seem to like async actions this.store.dispatch(store.fetchAnnotationsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any - this.store.dispatch(store.fetchCollaboratorsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any } protected handleInitialized(): void { diff --git a/src/components/ReplyField/ReplyField.tsx b/src/components/ReplyField/ReplyField.tsx index 3c6984ef6..11fb15e30 100644 --- a/src/components/ReplyField/ReplyField.tsx +++ b/src/components/ReplyField/ReplyField.tsx @@ -1,13 +1,14 @@ import * as React from 'react'; import classnames from 'classnames'; +import debounce from 'lodash/debounce'; import { addMention, getActiveMentionForEditorState, } from 'box-ui-elements/es/components/form-elements/draft-js-mention-selector/utils'; -import fuzzySearch from 'box-ui-elements/es/utils/fuzzySearch'; import { DraftHandleValue, Editor, EditorState } from 'draft-js'; import PopupList from '../Popups/PopupList'; import { Collaborator } from '../../@types'; +import { DEFAULT_COLLAB_DEBOUNCE } from '../../constants'; import { VirtualElement } from '../Popups/Popper'; import './ReplyField.scss'; @@ -28,6 +29,7 @@ export type Props = { onChange: (editorState: EditorState) => void; placeholder?: string; setCursorPosition: (cursorPosition: number) => void; + updateCollaborators: (searchStr: string) => void; }; export type State = { @@ -42,6 +44,18 @@ export default class ReplyField extends React.Component { state: State = { activeItemIndex: 0, popupReference: null }; + updateCollaborators = debounce((editorState: EditorState): void => { + const { updateCollaborators } = this.props; + + const activeMention = getActiveMentionForEditorState(editorState); + const trimmedQuery = activeMention?.mentionString.trim(); + if (!trimmedQuery) { + return; + } + + updateCollaborators(trimmedQuery); + }, DEFAULT_COLLAB_DEBOUNCE); + componentDidUpdate({ editorState: prevEditorState }: Props): void { const { editorState } = this.props; @@ -54,33 +68,6 @@ export default class ReplyField extends React.Component { this.saveCursorPosition(); } - getCollaborators = (): Collaborator[] => { - const { collaborators, editorState } = this.props; - - const activeMention = getActiveMentionForEditorState(editorState); - if (!activeMention) { - return []; - } - - const trimmedQuery = activeMention.mentionString.trim(); - // fuzzySearch doesn't match anything if query length is less than 2 - // Compared to empty list, full list has a better user experience - if (trimmedQuery.length < 2) { - return collaborators; - } - - return collaborators.filter(({ item }) => { - if (!item) { - return false; - } - - const isNameMatch = fuzzySearch(trimmedQuery, item.name, 0); - const isEmailMatch = 'email' in item && fuzzySearch(trimmedQuery, item.email, 0); - - return isNameMatch || isEmailMatch; - }); - }; - getVirtualElement = (activeMention: Mention): VirtualElement | null => { const selection = window.getSelection(); if (!selection?.focusNode) { @@ -133,14 +120,14 @@ export default class ReplyField extends React.Component { handleChange = (nextEditorState: EditorState): void => { const { onChange } = this.props; + this.updateCollaborators(nextEditorState); onChange(nextEditorState); }; handleSelect = (index: number): void => { - const { editorState } = this.props; + const { collaborators, editorState } = this.props; const activeMention = getActiveMentionForEditorState(editorState); - const collaborators = this.getCollaborators(); const editorStateWithLink = addMention(editorState, activeMention, collaborators[index]); this.handleChange(editorStateWithLink); @@ -161,8 +148,9 @@ export default class ReplyField extends React.Component { }; handleArrow = (event: React.KeyboardEvent): number => { + const { collaborators } = this.props; const { popupReference } = this.state; - const { length } = this.getCollaborators(); + const { length } = collaborators; if (!popupReference || !length) { return 0; @@ -196,7 +184,7 @@ export default class ReplyField extends React.Component { }; render(): JSX.Element { - const { className, editorState, isDisabled, placeholder, ...rest } = this.props; + const { className, collaborators, editorState, isDisabled, placeholder, ...rest } = this.props; const { activeItemIndex, popupReference } = this.state; return ( @@ -217,7 +205,7 @@ export default class ReplyField extends React.Component { {popupReference && ( ({ export const mapDispatchToProps = { setCursorPosition: setCursorAction, + updateCollaborators: fetchCollaboratorsAction, }; export default connect(mapStateToProps, mapDispatchToProps)(ReplyField); diff --git a/src/constants.js b/src/constants.js index 25a7c6153..c595c1106 100644 --- a/src/constants.js +++ b/src/constants.js @@ -13,3 +13,5 @@ export const PLACEHOLDER_USER = { id: '0', email: '', }; + +export const DEFAULT_COLLAB_DEBOUNCE = 500; diff --git a/src/store/users/actions.ts b/src/store/users/actions.ts index 5a8f826e2..404eb180d 100644 --- a/src/store/users/actions.ts +++ b/src/store/users/actions.ts @@ -4,9 +4,9 @@ import { AppThunkAPI } from '../types'; import { Collaborator } from '../../@types'; import { getFileId } from '../options'; -export const fetchCollaboratorsAction = createAsyncThunk, undefined, AppThunkAPI>( +export const fetchCollaboratorsAction = createAsyncThunk, string, AppThunkAPI>( 'FETCH_COLLABORATORS', - async (arg, { extra, getState, signal }) => { + async (searchStr, { extra, getState, signal }) => { // Create a new client for each request const client = extra.api.getCollaboratorsAPI(); const state = getState(); @@ -20,6 +20,7 @@ export const fetchCollaboratorsAction = createAsyncThunk>((resolve, reject) => { client.getFileCollaborators(fileId, resolve, reject, { + filter_term: searchStr, // eslint-disable-line @typescript-eslint/camelcase include_groups: false, // eslint-disable-line @typescript-eslint/camelcase include_uploader_collabs: false, // eslint-disable-line @typescript-eslint/camelcase });